Skip to main content

gn_core/
engine.rs

1use crate::error::{GnError, Result};
2use crate::namespace::Namespace;
3use crate::note::Note;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::process::{Command, Stdio};
7use uuid::Uuid;
8
9pub struct NotesEngine {
10    pub repo_path: PathBuf,
11}
12
13impl NotesEngine {
14    pub fn new<P: AsRef<Path>>(repo_path: P) -> Self {
15        Self {
16            repo_path: repo_path.as_ref().to_path_buf(),
17        }
18    }
19
20    fn git_cmd(&self) -> Command {
21        let mut cmd = Command::new("git");
22        cmd.current_dir(&self.repo_path);
23        cmd
24    }
25
26    pub fn read_notes(&self, namespace: &Namespace) -> Result<Vec<Note>> {
27        let ref_path = namespace.ref_path();
28
29        let output = self.git_cmd().args(["ls-tree", "-r", &ref_path]).output()?;
30
31        if !output.status.success() {
32            // If the ref doesn't exist, we just have 0 notes.
33            return Ok(Vec::new());
34        }
35
36        let out_str = String::from_utf8(output.stdout)?;
37        let mut notes = Vec::new();
38
39        for line in out_str.lines() {
40            let parts: Vec<&str> = line.split_whitespace().collect();
41            if parts.len() < 4 {
42                continue;
43            }
44            let blob_hash = parts[2];
45
46            let cat_file = self
47                .git_cmd()
48                .args(["cat-file", "blob", blob_hash])
49                .output()?;
50
51            if cat_file.status.success() {
52                let blob_str = String::from_utf8(cat_file.stdout)?;
53                if let Ok(note) = serde_json::from_str::<Note>(&blob_str) {
54                    notes.push(note);
55                }
56            }
57        }
58
59        Ok(notes)
60    }
61
62    pub fn write_note(&self, note: &Note) -> Result<String> {
63        let note_json = serde_json::to_string(note)?;
64
65        // 1. Hash object
66        let mut hash_cmd = self.git_cmd();
67        hash_cmd
68            .args(["hash-object", "-w", "--stdin"])
69            .stdin(Stdio::piped())
70            .stdout(Stdio::piped());
71
72        let mut child = hash_cmd.spawn()?;
73        if let Some(mut stdin) = child.stdin.take() {
74            stdin.write_all(note_json.as_bytes())?;
75        }
76        let hash_output = child.wait_with_output()?;
77        let blob_hash = String::from_utf8(hash_output.stdout)?.trim().to_string();
78
79        let ref_path = note.namespace.ref_path();
80
81        // 2. Read existing tree or create new
82        let tree_cmd = self.git_cmd().args(["ls-tree", &ref_path]).output()?;
83
84        let mut tree_entries = String::new();
85        if tree_cmd.status.success() {
86            tree_entries = String::from_utf8(tree_cmd.stdout)?;
87        }
88
89        // Add or update the file named by UUID
90        let new_entry = format!("100644 blob {}\t{}\n", blob_hash, note.id);
91
92        // Filter out existing entry for this note id if updating
93        let mut new_tree_input = tree_entries
94            .lines()
95            .filter(|line| !line.ends_with(&note.id.to_string()))
96            .map(|line| format!("{}\n", line))
97            .collect::<String>();
98
99        new_tree_input.push_str(&new_entry);
100
101        // 3. mktree
102        let mut mktree_cmd = self.git_cmd();
103        mktree_cmd
104            .arg("mktree")
105            .stdin(Stdio::piped())
106            .stdout(Stdio::piped());
107
108        let mut child = mktree_cmd.spawn()?;
109        if let Some(mut stdin) = child.stdin.take() {
110            stdin.write_all(new_tree_input.as_bytes())?;
111        }
112        let mktree_output = child.wait_with_output()?;
113        let new_tree_hash = String::from_utf8(mktree_output.stdout)?.trim().to_string();
114
115        // 4. commit-tree
116        let mut commit_cmd = self.git_cmd();
117        commit_cmd.args([
118            "commit-tree",
119            &new_tree_hash,
120            "-m",
121            &format!("Update note {}", note.id),
122        ]);
123
124        // Find parent commit if ref exists
125        let rev_parse = self
126            .git_cmd()
127            .args(["rev-parse", "-q", "--verify", &ref_path])
128            .output()?;
129        if rev_parse.status.success() {
130            let parent_hash = String::from_utf8(rev_parse.stdout)?.trim().to_string();
131            commit_cmd.args(["-p", &parent_hash]);
132        }
133
134        let commit_output = commit_cmd.output()?;
135        let commit_hash = String::from_utf8(commit_output.stdout)?.trim().to_string();
136
137        // 5. update-ref
138        self.git_cmd()
139            .args(["update-ref", &ref_path, &commit_hash])
140            .output()?;
141
142        Ok(commit_hash)
143    }
144
145    pub fn delete_note(&self, note_id: Uuid, namespace: &Namespace) -> Result<()> {
146        let ref_path = namespace.ref_path();
147
148        let tree_cmd = self.git_cmd().args(["ls-tree", &ref_path]).output()?;
149        if !tree_cmd.status.success() {
150            return Err(GnError::NotFound(format!(
151                "Namespace {} not found",
152                ref_path
153            )));
154        }
155
156        let tree_entries = String::from_utf8(tree_cmd.stdout)?;
157        let mut new_tree_input = String::new();
158        let mut found = false;
159
160        for line in tree_entries.lines() {
161            if line.ends_with(&note_id.to_string()) {
162                found = true;
163            } else {
164                new_tree_input.push_str(line);
165                new_tree_input.push('\n');
166            }
167        }
168
169        if !found {
170            return Err(GnError::NotFound(format!("Note {} not found", note_id)));
171        }
172
173        let mut mktree_cmd = self.git_cmd();
174        mktree_cmd
175            .arg("mktree")
176            .stdin(Stdio::piped())
177            .stdout(Stdio::piped());
178        let mut child = mktree_cmd.spawn()?;
179        if let Some(mut stdin) = child.stdin.take() {
180            stdin.write_all(new_tree_input.as_bytes())?;
181        }
182        let mktree_output = child.wait_with_output()?;
183        let new_tree_hash = String::from_utf8(mktree_output.stdout)?.trim().to_string();
184
185        let mut commit_cmd = self.git_cmd();
186        commit_cmd.args([
187            "commit-tree",
188            &new_tree_hash,
189            "-m",
190            &format!("Delete note {}", note_id),
191        ]);
192
193        let rev_parse = self.git_cmd().args(["rev-parse", &ref_path]).output()?;
194        if rev_parse.status.success() {
195            let parent_hash = String::from_utf8(rev_parse.stdout)?.trim().to_string();
196            commit_cmd.args(["-p", &parent_hash]);
197        }
198
199        let commit_output = commit_cmd.output()?;
200        let commit_hash = String::from_utf8(commit_output.stdout)?.trim().to_string();
201
202        self.git_cmd()
203            .args(["update-ref", &ref_path, &commit_hash])
204            .output()?;
205
206        Ok(())
207    }
208
209    pub fn list_notes_for_commit(&self, commit: &str, namespace: &Namespace) -> Result<Vec<Note>> {
210        let all_notes = self.read_notes(namespace)?;
211        let filtered = all_notes
212            .into_iter()
213            .filter(|n| n.commit == commit)
214            .collect();
215        Ok(filtered)
216    }
217}