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 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 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 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 let new_entry = format!("100644 blob {}\t{}\n", blob_hash, note.id);
91
92 let mut new_tree_input = tree_entries
94 .lines()
95 .filter(|line| !line.ends_with(¬e.id.to_string()))
96 .map(|line| format!("{}\n", line))
97 .collect::<String>();
98
99 new_tree_input.push_str(&new_entry);
100
101 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 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 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 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(¬e_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}