feat: can now do file operations
All checks were successful
continuous-integration/drone Build is passing
All checks were successful
continuous-integration/drone Build is passing
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -389,7 +389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "noil"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
|
@@ -1,6 +1,9 @@
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::commit::write_changes;
|
||||
use crate::{
|
||||
cli::edit::apply,
|
||||
commit::{Action, print_changes},
|
||||
};
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
pub struct ApplyCommand {}
|
||||
@@ -14,8 +17,11 @@ impl ApplyCommand {
|
||||
|
||||
let input = String::from_utf8_lossy(&buffer);
|
||||
|
||||
write_changes(&input).await?;
|
||||
|
||||
Ok(())
|
||||
let action = print_changes(&input).await?;
|
||||
match action {
|
||||
Action::Quit => Ok(()),
|
||||
Action::Apply { original } => apply(&original).await,
|
||||
Action::Edit => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,21 @@
|
||||
use std::{env::temp_dir, path::PathBuf};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use ansi_term::Color;
|
||||
use anyhow::{Context, bail};
|
||||
use clap::Parser;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
use crate::{commit::write_changes, encode_rand, output::get_outputs};
|
||||
use crate::{
|
||||
commit::{Action, print_changes},
|
||||
encode_rand,
|
||||
models::Operation,
|
||||
output::get_outputs,
|
||||
parse,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct EditCommand {
|
||||
@@ -42,21 +53,219 @@ impl EditCommand {
|
||||
|
||||
let editor = std::env::var("EDITOR").context("EDITOR not found in env")?;
|
||||
|
||||
let mut cmd = tokio::process::Command::new(editor.trim());
|
||||
cmd.arg(&file_path);
|
||||
loop {
|
||||
let mut cmd = tokio::process::Command::new(editor.trim());
|
||||
cmd.arg(&file_path);
|
||||
|
||||
let mut process = cmd.spawn()?;
|
||||
let status = process.wait().await.context("editor closed prematurely")?;
|
||||
if !status.success() {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
anyhow::bail!("editor exited: {code}");
|
||||
let mut process = cmd.spawn()?;
|
||||
let status = process.wait().await.context("editor closed prematurely")?;
|
||||
if !status.success() {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
anyhow::bail!("editor exited: {code}");
|
||||
}
|
||||
|
||||
let noil_content = tokio::fs::read_to_string(&file_path)
|
||||
.await
|
||||
.context("read noil file")?;
|
||||
|
||||
let res = print_changes(&noil_content).await;
|
||||
|
||||
let action = match res {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Invalid operation\n{}\n\nreverting to edit on any key press: ",
|
||||
Color::Red.normal().paint(format!("{e:?}"))
|
||||
);
|
||||
|
||||
wait_user().await?;
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match action {
|
||||
Action::Quit => return Ok(()),
|
||||
Action::Apply { original } => {
|
||||
return apply(&original).await;
|
||||
}
|
||||
Action::Edit => continue,
|
||||
}
|
||||
}
|
||||
|
||||
let noil_content = tokio::fs::read_to_string(&file_path)
|
||||
.await
|
||||
.context("read noil file")?;
|
||||
|
||||
write_changes(&noil_content).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_user() -> Result<(), anyhow::Error> {
|
||||
let mut stderr = std::io::stderr();
|
||||
stderr.flush()?;
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut reader = BufReader::new(stdin);
|
||||
let mut input_buf = String::new();
|
||||
reader.read_line(&mut input_buf).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// the philosphy behind apply is that we try unlike normal file system operations to be idempotent.
|
||||
/// This is mainly for 2 reasons.
|
||||
///
|
||||
/// 1. A lot of operations are processed, stopping in the middle because of an error would ruing your previous procedure that you now have to go back and fix
|
||||
/// 2. A .noil recipe can be rerun, having small issues disrupt the work would be counterproductive, as the .noil language is not powerful enough to handle the flexibility required for file checking
|
||||
///
|
||||
/// All in all apply is mostly idempotent, and won't override files, it tries to be as non destructive as possible. For example move will only throw a warning if the source file doesn't exists, but the destination does
|
||||
pub async fn apply(input: &str) -> anyhow::Result<()> {
|
||||
eprintln!("applying changes");
|
||||
|
||||
let noil_index = parse::parse_input(input).context("parse input")?;
|
||||
|
||||
for file in &noil_index.files {
|
||||
let path = &file.path;
|
||||
match &file.entry.operation {
|
||||
Operation::Existing { .. } => {
|
||||
// Noop
|
||||
}
|
||||
Operation::Add => {
|
||||
tracing::debug!("creating file");
|
||||
|
||||
if path.exists() {
|
||||
tracing::warn!("path already exists");
|
||||
continue;
|
||||
}
|
||||
|
||||
// is dir
|
||||
if path.to_string_lossy().ends_with("/") {
|
||||
tokio::fs::create_dir_all(&path)
|
||||
.await
|
||||
.context("add directory")?;
|
||||
tracing::info!("added directory");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(&parent)
|
||||
.await
|
||||
.context("create parent dir for add file")?;
|
||||
}
|
||||
|
||||
tokio::fs::File::create(&path).await.context("add file")?;
|
||||
|
||||
tracing::info!("added file");
|
||||
}
|
||||
Operation::Copy { index } => {
|
||||
tracing::debug!("copying file");
|
||||
|
||||
let existing = noil_index.get_existing(index).ok_or(anyhow::anyhow!(
|
||||
"entry with index: '{}' does not exist for copy",
|
||||
index
|
||||
))?;
|
||||
if !existing.path.exists() {
|
||||
bail!("existing does not exist for copy")
|
||||
}
|
||||
|
||||
if path.exists() {
|
||||
tracing::warn!("path already exists, cannot copy");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
tokio::fs::create_dir_all(&parent)
|
||||
.await
|
||||
.context("create parent dir for copy")?;
|
||||
}
|
||||
|
||||
if existing.path.is_dir() {
|
||||
tracing::debug!("copying dir");
|
||||
copy(&existing.path, path).await?;
|
||||
}
|
||||
|
||||
tokio::fs::copy(&existing.path, &path)
|
||||
.await
|
||||
.context("copy file for copy")?;
|
||||
}
|
||||
Operation::Delete { .. } => {
|
||||
tracing::debug!("deleting file");
|
||||
|
||||
if !path.exists() {
|
||||
tracing::warn!("path doesn't exist");
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
tokio::fs::remove_dir_all(&path)
|
||||
.await
|
||||
.context("remove path for delete")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::fs::remove_file(&path)
|
||||
.await
|
||||
.context("remove file for delete")?
|
||||
}
|
||||
Operation::Move { index } => {
|
||||
tracing::debug!("moving file");
|
||||
|
||||
let existing = noil_index.get_existing(index);
|
||||
|
||||
if existing.is_none() {
|
||||
// If the destination exists, but the existing one doesn't we assume it has already been moved
|
||||
if path.exists() {
|
||||
tracing::warn!("destination file looks to already have been moved");
|
||||
continue;
|
||||
}
|
||||
|
||||
anyhow::bail!("neither existing, or destination exists for move");
|
||||
}
|
||||
let existing = existing.unwrap();
|
||||
|
||||
if path.exists() {
|
||||
anyhow::bail!("destination already exists cannot move");
|
||||
}
|
||||
|
||||
tokio::fs::rename(&existing.path, path)
|
||||
.await
|
||||
.context("move path")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy(source: &Path, dest: &Path) -> anyhow::Result<()> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
for entry in walkdir::WalkDir::new(source) {
|
||||
let entry = entry?;
|
||||
paths.push(entry.path().strip_prefix(source)?.to_path_buf());
|
||||
}
|
||||
|
||||
for path in paths {
|
||||
let source = source.join(&path);
|
||||
let dest = dest.join(&path);
|
||||
|
||||
copy_path(&source, &dest).await.context(anyhow::anyhow!(
|
||||
"copy path: (src: {}, dest: {})",
|
||||
source.display(),
|
||||
dest.display()
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy_path(src: &Path, dest: &Path) -> anyhow::Result<()> {
|
||||
if let Some(parent) = dest.parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.context("copy dir create parent dir")?;
|
||||
}
|
||||
|
||||
if src.is_dir() {
|
||||
tokio::fs::create_dir_all(&dest).await.context("copy dir")?;
|
||||
}
|
||||
|
||||
if dest.is_file() {
|
||||
tokio::fs::copy(&src, &dest).await.context("copy file")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@@ -1,11 +1,21 @@
|
||||
use std::path::Path;
|
||||
|
||||
use ansi_term::Color;
|
||||
use anyhow::Context;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
use crate::{models::Operation, parse::parse};
|
||||
use std::io::Write;
|
||||
|
||||
pub async fn write_changes(input: &str) -> anyhow::Result<()> {
|
||||
let noil_index = parse(input).context("parse input")?;
|
||||
use crate::{models::Operation, parse::parse_input};
|
||||
|
||||
pub enum Action {
|
||||
Quit,
|
||||
Apply { original: String },
|
||||
Edit,
|
||||
}
|
||||
|
||||
pub async fn print_changes(input: &str) -> anyhow::Result<Action> {
|
||||
let noil_index = parse_input(input).context("parse input")?;
|
||||
|
||||
fn print_op(key: &str, index: Option<&str>, path: Option<&Path>) {
|
||||
match index {
|
||||
@@ -54,8 +64,47 @@ pub async fn write_changes(input: &str) -> anyhow::Result<()> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
print!("\nApply changes? (y/N): ");
|
||||
println!();
|
||||
eprint!("\nApply changes? (y (yes) / n (abort) / E (edit)): ");
|
||||
let mut stderr = std::io::stderr();
|
||||
stderr.flush()?;
|
||||
|
||||
Ok(())
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut reader = BufReader::new(stdin);
|
||||
let mut input_buf = String::new();
|
||||
|
||||
reader.read_line(&mut input_buf).await?;
|
||||
let trimmed = input_buf.trim().to_lowercase();
|
||||
|
||||
match trimmed.as_str() {
|
||||
"y" => {
|
||||
println!("Confirmed.");
|
||||
|
||||
Ok(Action::Apply {
|
||||
original: input.to_string(),
|
||||
})
|
||||
}
|
||||
"n" => {
|
||||
println!("Aborted.");
|
||||
Ok(Action::Quit)
|
||||
}
|
||||
"e" | "" => {
|
||||
println!("Edit");
|
||||
|
||||
Ok(Action::Edit)
|
||||
}
|
||||
_ => {
|
||||
println!("Invalid input: {}", Color::Red.normal().paint(trimmed));
|
||||
|
||||
eprint!("press enter to edit: ");
|
||||
let mut stderr = std::io::stderr();
|
||||
stderr.flush()?;
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut reader = BufReader::new(stdin);
|
||||
let mut input_buf = String::new();
|
||||
reader.read_line(&mut input_buf).await?;
|
||||
|
||||
Ok(Action::Edit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,10 +4,10 @@ use anyhow::Context;
|
||||
|
||||
use crate::models;
|
||||
|
||||
use super::parse::parse;
|
||||
use super::parse::parse_input;
|
||||
|
||||
pub(crate) fn format(input: &str) -> anyhow::Result<String> {
|
||||
let noil_index = parse(input).context("parse input")?;
|
||||
let noil_index = parse_input(input).context("parse input")?;
|
||||
|
||||
let max_op_len = noil_index
|
||||
.files
|
||||
|
@@ -7,6 +7,15 @@ pub struct Buffer {
|
||||
pub(crate) files: Vec<File>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn get_existing(&self, index: &str) -> Option<&File> {
|
||||
self.files.iter().find(|f| match &f.entry.operation {
|
||||
Operation::Existing { index: idx } => idx == index,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct File {
|
||||
pub(crate) path: PathBuf,
|
||||
@@ -100,7 +109,7 @@ abc : /var/my
|
||||
ecd : /var/my/path
|
||||
"#;
|
||||
|
||||
let output = parse::parse(input)?;
|
||||
let output = parse::parse_input(input)?;
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
Buffer {
|
||||
@@ -140,7 +149,7 @@ A : /var/my/path/new-path
|
||||
ADD : /var/my/path/new-long-path
|
||||
"#;
|
||||
|
||||
let output = parse::parse(input)?;
|
||||
let output = parse::parse_input(input)?;
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
Buffer {
|
||||
@@ -194,7 +203,7 @@ C abc : /var/my/path/copy-into
|
||||
COPY ecd : /var/my/path/copy-into-long
|
||||
"#;
|
||||
|
||||
let output = parse::parse(input)?;
|
||||
let output = parse::parse_input(input)?;
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
Buffer {
|
||||
@@ -250,7 +259,7 @@ DEL ecd : /var/my/path
|
||||
DELETE ecd : /var/my/path
|
||||
"#;
|
||||
|
||||
let output = parse::parse(input)?;
|
||||
let output = parse::parse_input(input)?;
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
Buffer {
|
||||
@@ -301,7 +310,7 @@ MOVE ecd : /var/my/some-different-place
|
||||
RENAME ecd : /var/my/some-different-place
|
||||
"#;
|
||||
|
||||
let output = parse::parse(input)?;
|
||||
let output = parse::parse_input(input)?;
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
Buffer {
|
||||
|
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use crate::models;
|
||||
|
||||
pub(crate) fn parse(input: &str) -> anyhow::Result<models::Buffer> {
|
||||
pub(crate) fn parse_input(input: &str) -> anyhow::Result<models::Buffer> {
|
||||
let mut files = Vec::default();
|
||||
// We are keeping parsing simple. For each line take any non empty lines, the first part should be an index. This is where the magic happens, if it contains special tokens handle accordingly, the path always comes after a :.
|
||||
for line in input.lines() {
|
||||
|
Reference in New Issue
Block a user