veridian_kernel/devtools/git/
commands.rs1use alloc::{
7 collections::BTreeMap,
8 string::{String, ToString},
9 vec::Vec,
10};
11
12use super::{
13 objects::{Blob, Commit, GitObject, ObjectId, ObjectType, Person, Tree, TreeEntry},
14 refs::{RefStore, RefValue},
15};
16
17#[derive(Debug, Clone)]
19pub(crate) struct IndexEntry {
20 pub(crate) path: String,
21 pub(crate) id: ObjectId,
22 pub(crate) mode: u32,
23 pub(crate) size: u64,
24}
25
26pub(crate) struct Repository {
28 objects: BTreeMap<ObjectId, Vec<u8>>,
30 pub(crate) refs: RefStore,
32 index: Vec<IndexEntry>,
34 pub(crate) workdir: String,
36 pub(crate) config: GitConfig,
38}
39
40#[derive(Debug, Clone)]
42pub(crate) struct GitConfig {
43 pub(crate) user_name: String,
44 pub(crate) user_email: String,
45}
46
47impl Default for GitConfig {
48 fn default() -> Self {
49 Self {
50 user_name: "VeridianOS User".to_string(),
51 user_email: "user@veridian.local".to_string(),
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
58pub(crate) struct DiffHunk {
59 pub(crate) old_start: usize,
60 pub(crate) old_count: usize,
61 pub(crate) new_start: usize,
62 pub(crate) new_count: usize,
63 pub(crate) lines: Vec<DiffLine>,
64}
65
66#[derive(Debug, Clone)]
68pub(crate) enum DiffLine {
69 Context(String),
70 Added(String),
71 Removed(String),
72}
73
74#[derive(Debug, Clone)]
76pub(crate) struct LogEntry {
77 pub(crate) id: ObjectId,
78 pub(crate) author: String,
79 pub(crate) timestamp: u64,
80 pub(crate) message: String,
81}
82
83impl Repository {
84 pub(crate) fn init(workdir: &str) -> Self {
86 Self {
87 objects: BTreeMap::new(),
88 refs: RefStore::new(),
89 index: Vec::new(),
90 workdir: workdir.to_string(),
91 config: GitConfig::default(),
92 }
93 }
94
95 pub(crate) fn store_object(&mut self, obj: &GitObject) -> ObjectId {
97 let id = obj.compute_id();
98 self.objects.entry(id).or_insert_with(|| obj.serialize());
99 id
100 }
101
102 pub(crate) fn get_object(&self, id: &ObjectId) -> Option<GitObject> {
104 let data = self.objects.get(id)?;
105 GitObject::deserialize(data)
106 }
107
108 pub(crate) fn has_object(&self, id: &ObjectId) -> bool {
110 self.objects.contains_key(id)
111 }
112
113 pub(crate) fn object_count(&self) -> usize {
115 self.objects.len()
116 }
117
118 pub(crate) fn add(&mut self, path: &str, content: &[u8]) -> ObjectId {
124 let blob = Blob::new(content.to_vec());
125 let obj = blob.to_object();
126 let id = self.store_object(&obj);
127
128 if let Some(entry) = self.index.iter_mut().find(|e| e.path == path) {
130 entry.id = id;
131 entry.size = content.len() as u64;
132 } else {
133 self.index.push(IndexEntry {
134 path: path.to_string(),
135 id,
136 mode: 0o100644,
137 size: content.len() as u64,
138 });
139 }
140
141 id
142 }
143
144 pub(crate) fn commit(&mut self, message: &str) -> Option<ObjectId> {
146 if self.index.is_empty() {
147 return None;
148 }
149
150 let tree_id = self.build_tree_from_index();
152
153 let timestamp = 0; let author = Person::new(&self.config.user_name, &self.config.user_email, timestamp);
156 let mut commit = Commit::new(tree_id, author, message);
157
158 if let Some(head_id) = self.refs.head() {
160 commit.parents.push(*head_id);
161 }
162
163 let obj = commit.to_object();
164 let commit_id = self.store_object(&obj);
165
166 match self.refs.get("HEAD") {
168 Some(RefValue::Symbolic(target)) => {
169 let target = target.clone();
170 self.refs.set_direct(&target, commit_id);
171 }
172 _ => {
173 self.refs.set_direct("HEAD", commit_id);
174 }
175 }
176
177 Some(commit_id)
178 }
179
180 fn build_tree_from_index(&mut self) -> ObjectId {
182 let mut tree = Tree::new();
183
184 let mut sorted_index = self.index.clone();
186 sorted_index.sort_by(|a, b| a.path.cmp(&b.path));
187
188 for entry in &sorted_index {
189 tree.add_entry(TreeEntry::new(entry.mode, &entry.path, entry.id));
191 }
192
193 let obj = tree.to_object();
194 self.store_object(&obj)
195 }
196
197 pub(crate) fn log(&self, max_entries: usize) -> Vec<LogEntry> {
199 let mut entries = Vec::new();
200 let mut current = match self.refs.head() {
201 Some(id) => *id,
202 None => return entries,
203 };
204
205 for _ in 0..max_entries {
206 let obj = match self.get_object(¤t) {
207 Some(o) if o.obj_type == ObjectType::Commit => o,
208 _ => break,
209 };
210
211 let commit = match Commit::from_data(&obj.data) {
212 Some(c) => c,
213 None => break,
214 };
215
216 entries.push(LogEntry {
217 id: current,
218 author: alloc::format!("{} <{}>", commit.author.name, commit.author.email),
219 timestamp: commit.author.timestamp,
220 message: commit.message.clone(),
221 });
222
223 current = match commit.parents.first() {
225 Some(p) => *p,
226 None => break,
227 };
228 }
229
230 entries
231 }
232
233 pub(crate) fn diff_blobs(&self, old: &[u8], new: &[u8]) -> Vec<DiffHunk> {
235 let old_lines: Vec<&str> = core::str::from_utf8(old).unwrap_or("").lines().collect();
236 let new_lines: Vec<&str> = core::str::from_utf8(new).unwrap_or("").lines().collect();
237
238 let mut hunks = Vec::new();
240 let mut lines = Vec::new();
241 let mut old_idx = 0;
242 let mut new_idx = 0;
243
244 while old_idx < old_lines.len() || new_idx < new_lines.len() {
245 if old_idx < old_lines.len()
246 && new_idx < new_lines.len()
247 && old_lines[old_idx] == new_lines[new_idx]
248 {
249 lines.push(DiffLine::Context(old_lines[old_idx].to_string()));
250 old_idx += 1;
251 new_idx += 1;
252 } else if old_idx < old_lines.len()
253 && (new_idx >= new_lines.len()
254 || !new_lines[new_idx..].contains(&old_lines[old_idx]))
255 {
256 lines.push(DiffLine::Removed(old_lines[old_idx].to_string()));
257 old_idx += 1;
258 } else if new_idx < new_lines.len() {
259 lines.push(DiffLine::Added(new_lines[new_idx].to_string()));
260 new_idx += 1;
261 }
262 }
263
264 if !lines.is_empty() {
265 hunks.push(DiffHunk {
266 old_start: 1,
267 old_count: old_lines.len(),
268 new_start: 1,
269 new_count: new_lines.len(),
270 lines,
271 });
272 }
273
274 hunks
275 }
276
277 pub(crate) fn branch_list(&self) -> Vec<String> {
279 self.refs.branches()
280 }
281
282 pub(crate) fn branch_create(&mut self, name: &str) -> bool {
283 if let Some(head_id) = self.refs.head() {
284 let id = *head_id;
285 self.refs.create_branch(name, id);
286 true
287 } else {
288 false
289 }
290 }
291
292 pub(crate) fn branch_delete(&mut self, name: &str) -> bool {
293 let ref_name = alloc::format!("refs/heads/{}", name);
294 self.refs.delete(&ref_name)
295 }
296
297 pub(crate) fn checkout(&mut self, branch: &str) -> bool {
299 self.refs.checkout_branch(branch)
300 }
301
302 pub(crate) fn status(&self) -> Vec<String> {
304 let mut lines = Vec::new();
305
306 if let Some(branch) = self.refs.current_branch() {
307 lines.push(alloc::format!("On branch {}", branch));
308 } else {
309 lines.push("HEAD detached".to_string());
310 }
311
312 if self.index.is_empty() {
313 lines.push("nothing to commit".to_string());
314 } else {
315 lines.push(alloc::format!(
316 "Changes to be committed ({} files):",
317 self.index.len()
318 ));
319 for entry in &self.index {
320 lines.push(alloc::format!(" new file: {}", entry.path));
321 }
322 }
323
324 lines
325 }
326
327 pub(crate) fn tag_create(&mut self, name: &str) -> bool {
329 if let Some(head_id) = self.refs.head() {
330 let id = *head_id;
331 self.refs.create_tag(name, id);
332 true
333 } else {
334 false
335 }
336 }
337
338 pub(crate) fn index_count(&self) -> usize {
340 self.index.len()
341 }
342}
343
344#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_repository_init() {
354 let repo = Repository::init("/tmp/test");
355 assert_eq!(repo.workdir, "/tmp/test");
356 assert_eq!(repo.object_count(), 0);
357 assert_eq!(repo.index_count(), 0);
358 }
359
360 #[test]
361 fn test_add_file() {
362 let mut repo = Repository::init("/tmp/test");
363 let id = repo.add("hello.txt", b"Hello, World!\n");
364 assert_ne!(id, ObjectId::ZERO);
365 assert_eq!(repo.index_count(), 1);
366 assert!(repo.has_object(&id));
367 }
368
369 #[test]
370 fn test_add_overwrites_same_path() {
371 let mut repo = Repository::init("/tmp/test");
372 repo.add("file.txt", b"v1");
373 repo.add("file.txt", b"v2");
374 assert_eq!(repo.index_count(), 1);
375 }
376
377 #[test]
378 fn test_commit() {
379 let mut repo = Repository::init("/tmp/test");
380 repo.add("file.txt", b"content");
381 let id = repo.commit("Initial commit");
382 assert!(id.is_some());
383
384 let obj = repo.get_object(&id.unwrap()).unwrap();
385 assert_eq!(obj.obj_type, ObjectType::Commit);
386 }
387
388 #[test]
389 fn test_commit_empty_index() {
390 let mut repo = Repository::init("/tmp/test");
391 assert!(repo.commit("Empty").is_none());
392 }
393
394 #[test]
395 fn test_commit_chain() {
396 let mut repo = Repository::init("/tmp/test");
397 repo.add("a.txt", b"a");
398 let first = repo.commit("First").unwrap();
399
400 repo.add("b.txt", b"b");
401 let second = repo.commit("Second").unwrap();
402
403 let obj = repo.get_object(&second).unwrap();
405 let commit = Commit::from_data(&obj.data).unwrap();
406 assert_eq!(commit.parents.len(), 1);
407 assert_eq!(commit.parents[0], first);
408 }
409
410 #[test]
411 fn test_log() {
412 let mut repo = Repository::init("/tmp/test");
413 repo.add("a.txt", b"a");
414 repo.commit("First commit");
415 repo.add("b.txt", b"b");
416 repo.commit("Second commit");
417
418 let log = repo.log(10);
419 assert_eq!(log.len(), 2);
420 assert!(log[0].message.contains("Second commit"));
421 assert!(log[1].message.contains("First commit"));
422 }
423
424 #[test]
425 fn test_log_empty() {
426 let repo = Repository::init("/tmp/test");
427 let log = repo.log(10);
428 assert!(log.is_empty());
429 }
430
431 #[test]
432 fn test_diff_blobs() {
433 let repo = Repository::init("/tmp/test");
434 let old = b"line1\nline2\nline3\n";
435 let new = b"line1\nmodified\nline3\n";
436 let hunks = repo.diff_blobs(old, new);
437 assert!(!hunks.is_empty());
438 }
439
440 #[test]
441 fn test_diff_identical() {
442 let repo = Repository::init("/tmp/test");
443 let content = b"same\n";
444 let hunks = repo.diff_blobs(content, content);
445 assert!(!hunks.is_empty());
447 }
448
449 #[test]
450 fn test_branch_operations() {
451 let mut repo = Repository::init("/tmp/test");
452 repo.add("file.txt", b"content");
453 repo.commit("Initial");
454
455 assert!(repo.branch_create("dev"));
456 let branches = repo.branch_list();
457 assert!(branches.contains(&"main".to_string()));
458 assert!(branches.contains(&"dev".to_string()));
459 }
460
461 #[test]
462 fn test_checkout() {
463 let mut repo = Repository::init("/tmp/test");
464 repo.add("file.txt", b"content");
465 repo.commit("Initial");
466 repo.branch_create("dev");
467
468 assert!(repo.checkout("dev"));
469 assert_eq!(repo.refs.current_branch(), Some("dev"));
470 }
471
472 #[test]
473 fn test_checkout_nonexistent() {
474 let mut repo = Repository::init("/tmp/test");
475 assert!(!repo.checkout("nonexistent"));
476 }
477
478 #[test]
479 fn test_branch_delete() {
480 let mut repo = Repository::init("/tmp/test");
481 repo.add("file.txt", b"content");
482 repo.commit("Initial");
483 repo.branch_create("temp");
484 assert!(repo.branch_delete("temp"));
485 assert!(!repo.branch_delete("temp"));
486 }
487
488 #[test]
489 fn test_status_empty() {
490 let repo = Repository::init("/tmp/test");
491 let status = repo.status();
492 assert!(status.iter().any(|s| s.contains("nothing to commit")));
493 }
494
495 #[test]
496 fn test_status_with_staged() {
497 let mut repo = Repository::init("/tmp/test");
498 repo.add("file.txt", b"content");
499 let status = repo.status();
500 assert!(status.iter().any(|s| s.contains("1 files")));
501 }
502
503 #[test]
504 fn test_tag_create() {
505 let mut repo = Repository::init("/tmp/test");
506 repo.add("file.txt", b"content");
507 repo.commit("Release");
508 assert!(repo.tag_create("v1.0"));
509 assert!(repo.refs.tags().contains(&"v1.0".to_string()));
510 }
511
512 #[test]
513 fn test_config_default() {
514 let config = GitConfig::default();
515 assert!(!config.user_name.is_empty());
516 assert!(!config.user_email.is_empty());
517 }
518
519 #[test]
520 fn test_store_and_retrieve_object() {
521 let mut repo = Repository::init("/tmp/test");
522 let obj = GitObject::new(ObjectType::Blob, b"hello".to_vec());
523 let id = repo.store_object(&obj);
524 let retrieved = repo.get_object(&id).unwrap();
525 assert_eq!(retrieved.obj_type, ObjectType::Blob);
526 assert_eq!(&retrieved.data, b"hello");
527 }
528}