source: gignore/src/template.rs@ d237199

multi_arg trunk
Last change on this file since d237199 was 1371fe9, checked in by Asher Mullaney <asher.mullaney@…>, 2 weeks ago

Fixed #5 and fmt'd

  • Property mode set to 100644
File size: 10.6 KB
Line 
1// IMPORTANT -- LEGAL NOTICE!
2
3// gignore: a .gitignore manipulation program
4// Copyright (C) 2026 Asher Mullaney \<asher.mullaney@gmail.com\>
5//
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19use std::{
20 fs,
21 io::{self, Write},
22 os::unix::fs::MetadataExt,
23 path::{Path, PathBuf},
24 process,
25};
26
27use anyhow::{Context, Error, Result};
28
29use serde::{Deserialize, Serialize};
30
31use super::{open_append, open_truncate};
32
33#[derive(Serialize, Deserialize, Debug)]
34pub(crate) struct Template {
35 source_file: PathBuf,
36 name: String,
37 append: bool,
38}
39
40impl Template {
41 pub(crate) fn new(source_file: &Path, name: &str, append: bool) -> Self {
42 Template {
43 source_file: source_file.to_owned(),
44 name: String::from(name),
45 append,
46 }
47 }
48
49 fn get_source_file(&self) -> &PathBuf {
50 &self.source_file
51 }
52 fn change_source_file<T: AsRef<Path>>(&mut self, new: T) {
53 self.source_file = new.as_ref().to_owned();
54 }
55 pub(crate) fn get_name(&self) -> &str {
56 &self.name
57 }
58 pub(crate) fn is_append(&self) -> bool {
59 self.append
60 }
61 fn save(&self) -> Result<()> {
62 let path = "/usr/lib/gignore/gignore_templates";
63
64 if !fs::exists(path).context("cannot search for `/usr/lib/gignore/gignore_templates`")? {
65 return Err(Error::msg(
66 "`/usr/lib/gignore/gignore_templates` does not exist",
67 ));
68 }
69
70 let mut file_handle = open_append(path.as_ref())
71 .context("cannot open `/usr/lib/gignore/gignore_templates`")?;
72
73 let write_string = serde_json::to_string(&self)
74 .context("cannot convert template to JSON using `serde_json`")?;
75
76 file_handle
77 .write_all(format!("{write_string}\n").as_bytes())
78 .context("cannot write to `/usr/lib/gignore/gignore_templates`")?;
79
80 Ok(())
81 }
82}
83
84fn lib_read() -> Result<Vec<Template>> {
85 let path = "/usr/lib/gignore/gignore_templates";
86
87 let mut template_vec: Vec<Template> = Vec::new();
88
89 if fs::read_to_string(path)
90 .context("cannot read `/usr/lib/gignore/gignore_templates`")?
91 .is_empty()
92 {
93 return Ok(template_vec);
94 }
95
96 for line in fs::read_to_string(path)
97 .context("cannot read `/usr/lib/gignore/gignore_templates`")?
98 .lines()
99 {
100 template_vec.push(
101 serde_json::from_str(&line.trim())
102 .context("cannot convert JSON to `template` instance")?,
103 );
104 }
105 Ok(template_vec)
106}
107
108pub(crate) fn find_name(name: &str) -> Result<Option<Template>> {
109 let template_vec: Vec<Template> = lib_read().context("cannot read template library")?;
110
111 for item in template_vec {
112 if item.get_name() == name {
113 return Ok(Some(item));
114 }
115 }
116 Ok(None)
117}
118
119pub(crate) fn remove(name: &str) -> Result<()> {
120 let path = "/usr/lib/gignore/gignore_templates";
121
122 let mut template_vec: Vec<Template> = lib_read().context("cannot read template library")?;
123
124 template_vec
125 .extract_if(.., |item| item.get_name() == name)
126 .for_each(drop);
127
128 let template = match find_name(name).context(format!(
129 "cannot search for name {name} in /usr/lib/gignore/gignore_templates"
130 ))? {
131 None => {
132 eprintln!(
133 "No template exists in `/usr/lib/gignore/gignore_templates` under the name `{name}`."
134 );
135 process::exit(0);
136 }
137 Some(t) => t,
138 };
139
140 clear().context("cannot clear template library file")?;
141
142 if template_vec.is_empty() {
143 println!("{path} is now empty.");
144 fs::write(path, b"").context("cannot write to `/usr/lib/gignore/gignore_templates`")?;
145 } else {
146 for item in template_vec {
147 item.save().context("cannot save template")?;
148 }
149 }
150
151 fs::remove_file(template.get_source_file()).context(format!(
152 "cannot remove `{}`",
153 template.get_source_file().display()
154 ))?;
155 Ok(())
156}
157
158pub(crate) fn add(mut template: Template) -> Result<()> {
159 if !fs::exists(template.get_source_file())
160 .context(format!("cannot search for `{}`", template.get_name()))?
161 {
162 return Err(Error::msg(format!(
163 "The source file specified, `{}`, does not exist.",
164 template.get_source_file().display()
165 )));
166 }
167
168 if (*(template.get_source_file()))
169 .to_str()
170 .expect("FATAL: Could not convert source_file to string!")
171 .split_terminator('.')
172 .nth(2)
173 != Some("ggnr")
174 {
175 eprintln!(
176 "WARNING: specified source file, `{}`, does not have the file extension for gignore templates (`.ggnr`). Continue anyway? [y/N]",
177 template.get_source_file().display()
178 );
179
180 let mut buf = String::new();
181 io::stdin()
182 .read_line(&mut buf)
183 .context("cannot read from stdin")?;
184
185 match &*buf.to_lowercase().trim() {
186 "y" => {
187 eprintln!("Continuing...");
188 }
189 _ => {
190 eprintln!("User denied operation. Exiting.");
191 process::exit(0);
192 }
193 }
194 };
195
196 let new_template_path = format!("/usr/lib/gignore/{}", template.get_source_file().display());
197
198 if fs::exists(&new_template_path).context(format!("cannot search for `{new_template_path}`"))? {
199 eprintln!("WARNING: the file `{new_template_path}` already exists. Overwrite it? [y/N]");
200
201 let mut buf = String::new();
202 io::stdin()
203 .read_line(&mut buf)
204 .context("cannot read from stdin")?;
205
206 match &*buf.to_lowercase().trim() {
207 "y" => {
208 eprintln!("Continuing...");
209
210 fs::remove_file(&new_template_path)
211 .context(format!("cannot remove {new_template_path}"))?
212 }
213 _ => {
214 eprintln!("User denied operation. Exiting.");
215 process::exit(0);
216 }
217 };
218 };
219
220 drop(
221 fs::File::create_new(&new_template_path)
222 .context(format!("cannot create file `{new_template_path}`"))?,
223 );
224
225 let bytes_written =
226 fs::copy(template.get_source_file(), &*new_template_path).context(format!(
227 "cannot copy bytes from `{}` to `{new_template_path}`",
228 template.get_source_file().display()
229 ))?;
230 if !bytes_written
231 == fs::metadata(template.get_source_file())
232 .context(format!(
233 "cannot read metadata of `{}`",
234 template.get_source_file().display()
235 ))?
236 .size()
237 {
238 eprintln!(
239 "WARNING: Not all bytes of `{}` were written to `{new_template_path}`. ",
240 template.get_source_file().display()
241 );
242 }
243
244 if find_name(template.get_name())
245 .context("cannot search for name in template library")?
246 .is_some()
247 {
248 return Err(Error::msg(format!(
249 "A template with the name `{}` already exists!",
250 template.get_name()
251 )));
252 }
253 template.change_source_file(new_template_path);
254
255 template.save().context("cannot save template")?;
256
257 Ok(())
258}
259
260fn get_template_contents(name: &str) -> Result<String> {
261 let template = find_name(name).context("cannot search for name in template library")?;
262
263 Ok(fs::read_to_string(
264 match template {
265 Some(ref template) => template,
266 None => return Err(Error::msg(format!("No template under name `{name}`"))),
267 }
268 .get_source_file(),
269 )
270 .context(format!(
271 "cannot read {}",
272 template.unwrap().get_source_file().display()
273 ))?)
274}
275
276pub(crate) fn clear() -> Result<()> {
277 let path = "/usr/lib/gignore/gignore_templates";
278
279 open_truncate(path.as_ref())
280 .context("cannot open `/usr/lib/gignore/gignore_templates` in `truncate` mode")?
281 .write_all(b"")
282 .context("cannot write to /usr/lib/gignore/gignore_templates")?;
283
284 Ok(())
285}
286
287pub(crate) fn gitignore_write(name: &str) -> Result<()> {
288 let template = find_name(name).context("cannot search for name in template library")?;
289
290 if match template {
291 Some(template) => template.is_append(),
292 None => return Err(Error::msg(format!("No template under name `{name}`"))),
293 } {
294 let mut file_handle = open_append(".gitignore".as_ref())
295 .context("cannot open `.gitignore` in `append` mode")?;
296
297 file_handle
298 .write_all(
299 get_template_contents(name)
300 .context("cannot retrieve contents of template")?
301 .as_bytes(),
302 )
303 .context("cannot write to `.gitignore`")?;
304
305 return Ok(());
306 }
307
308 let mut file_handle = open_truncate(".gitignore".as_ref())
309 .context("cannot open `.gitignore` in `truncate` mode")?;
310
311 file_handle
312 .write_all(
313 get_template_contents(name)
314 .context("cannot retrieve contents of template")?
315 .as_bytes(),
316 )
317 .context("cannot write to `.gitignore`")?;
318
319 Ok(())
320}
321
322pub(crate) fn list(oneline: bool) -> Result<()> {
323 if !oneline {
324 for item in lib_read().context("cannot read template library")? {
325 if item.is_append() {
326 println!(
327 "{}: {} <append>",
328 item.get_name(),
329 item.get_source_file().display()
330 );
331 } else {
332 println!("{}: {}", item.get_name(), item.get_source_file().display());
333 }
334 }
335 return Ok(());
336 }
337
338 println!();
339
340 for item in lib_read().context("cannot read template library")? {
341 if item.is_append() {
342 print!(
343 "{}: {} (append), ",
344 item.get_name(),
345 item.get_source_file().display()
346 );
347 } else {
348 print!(
349 "{}: {}, ",
350 item.get_name(),
351 item.get_source_file().display()
352 );
353 }
354 }
355 println!();
356
357 Ok(())
358}
Note: See TracBrowser for help on using the repository browser.