source: gignore/src/template.rs

trunk
Last change on this file was 3cf1172, checked in by Asher Mullaney <asher.mullaney@…>, 10 days ago

Resolved #4. Also prettified 'Continue anyway' prompt.

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