Compare commits
7 Commits
v0.0.4
...
3895791138
Author | SHA1 | Date | |
---|---|---|---|
|
3895791138 | ||
3fdd9f93a9
|
|||
139aa6d75e
|
|||
a71672823c
|
|||
c0bd8c0d36
|
|||
dadec333d4 | |||
18411bc52c
|
15
CHANGELOG.md
15
CHANGELOG.md
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2025-08-03
|
||||
|
||||
### Added
|
||||
- add file opener with chooser
|
||||
- update demo
|
||||
- add help text for preview
|
||||
|
||||
### Other
|
||||
- remove cockroach
|
||||
|
||||
## [0.0.5] - 2025-07-26
|
||||
|
||||
### Added
|
||||
- actually copy files
|
||||
|
||||
## [0.0.4] - 2025-07-26
|
||||
|
||||
### Added
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -389,7 +389,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "noil"
|
||||
version = "0.0.3"
|
||||
version = "0.0.5"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
|
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.4"
|
||||
version = "0.1.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
noil = { path = "crates/noil" }
|
||||
|
BIN
assets/demo.gif
BIN
assets/demo.gif
Binary file not shown.
Before Width: | Height: | Size: 631 KiB After Width: | Height: | Size: 1.4 MiB |
@@ -1,12 +1,20 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::{
|
||||
cli::edit::apply,
|
||||
cli::edit::{ApplyOptions, apply},
|
||||
commit::{Action, print_changes},
|
||||
};
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
pub struct ApplyCommand {}
|
||||
pub struct ApplyCommand {
|
||||
#[arg(long = "commit")]
|
||||
commit: bool,
|
||||
|
||||
#[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")]
|
||||
chooser_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ApplyCommand {
|
||||
pub async fn execute(&self) -> anyhow::Result<()> {
|
||||
@@ -17,11 +25,33 @@ impl ApplyCommand {
|
||||
|
||||
let input = String::from_utf8_lossy(&buffer);
|
||||
|
||||
let action = print_changes(&input).await?;
|
||||
match action {
|
||||
if !self.commit {
|
||||
let action = print_changes(&input, !self.commit).await?;
|
||||
let res = match action {
|
||||
Action::Quit => Ok(()),
|
||||
Action::Apply { original } => apply(&original).await,
|
||||
Action::Apply { original } => {
|
||||
apply(
|
||||
&original,
|
||||
ApplyOptions {
|
||||
chooser_file: self.chooser_file.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
Action::Edit => todo!(),
|
||||
};
|
||||
|
||||
eprintln!("\nin preview mode: add (--commit) to perform actions");
|
||||
|
||||
res
|
||||
} else {
|
||||
apply(
|
||||
&input,
|
||||
ApplyOptions {
|
||||
chooser_file: self.chooser_file.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,10 +17,15 @@ use crate::{
|
||||
parse,
|
||||
};
|
||||
|
||||
const PREVIEW: bool = false;
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct EditCommand {
|
||||
#[arg()]
|
||||
path: PathBuf,
|
||||
|
||||
#[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")]
|
||||
chooser_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl EditCommand {
|
||||
@@ -68,7 +73,7 @@ impl EditCommand {
|
||||
.await
|
||||
.context("read noil file")?;
|
||||
|
||||
let res = print_changes(&noil_content).await;
|
||||
let res = print_changes(&noil_content, PREVIEW).await;
|
||||
|
||||
let action = match res {
|
||||
Ok(a) => a,
|
||||
@@ -87,7 +92,13 @@ impl EditCommand {
|
||||
match action {
|
||||
Action::Quit => return Ok(()),
|
||||
Action::Apply { original } => {
|
||||
return apply(&original).await;
|
||||
return apply(
|
||||
&original,
|
||||
ApplyOptions {
|
||||
chooser_file: self.chooser_file.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Action::Edit => continue,
|
||||
}
|
||||
@@ -105,6 +116,11 @@ async fn wait_user() -> Result<(), anyhow::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ApplyOptions {
|
||||
pub chooser_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// the philosphy behind apply is that we try unlike normal file system operations to be idempotent.
|
||||
/// This is mainly for 2 reasons.
|
||||
///
|
||||
@@ -112,17 +128,27 @@ async fn wait_user() -> Result<(), anyhow::Error> {
|
||||
/// 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<()> {
|
||||
pub async fn apply(input: &str, options: ApplyOptions) -> anyhow::Result<()> {
|
||||
eprintln!("applying changes");
|
||||
|
||||
let noil_index = parse::parse_input(input).context("parse input")?;
|
||||
|
||||
let mut open_files = Vec::new();
|
||||
|
||||
for file in &noil_index.files {
|
||||
let path = &file.path;
|
||||
match &file.entry.operation {
|
||||
Operation::Existing { .. } => {
|
||||
// Noop
|
||||
}
|
||||
Operation::Open { .. } => {
|
||||
if path.to_string_lossy().ends_with("/") {
|
||||
// We can't open directories, so they're skipped
|
||||
continue;
|
||||
}
|
||||
|
||||
open_files.push(path);
|
||||
}
|
||||
Operation::Add => {
|
||||
tracing::debug!("creating file");
|
||||
|
||||
@@ -175,6 +201,7 @@ pub async fn apply(input: &str) -> anyhow::Result<()> {
|
||||
if existing.path.is_dir() {
|
||||
tracing::debug!("copying dir");
|
||||
copy(&existing.path, path).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
tokio::fs::copy(&existing.path, &path)
|
||||
@@ -227,6 +254,31 @@ pub async fn apply(input: &str) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chooser_file) = &options.chooser_file {
|
||||
tracing::debug!("creating chooser file");
|
||||
if let Some(parent) = chooser_file.parent()
|
||||
&& !chooser_file.exists()
|
||||
{
|
||||
tokio::fs::create_dir_all(parent)
|
||||
.await
|
||||
.context("parent dir for chooser file")?;
|
||||
}
|
||||
|
||||
let mut file = tokio::fs::File::create(chooser_file)
|
||||
.await
|
||||
.context("create new chooser file")?;
|
||||
|
||||
let open_files = open_files
|
||||
.iter()
|
||||
.map(|i| i.display().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
file.write_all(open_files.join("\n").as_bytes())
|
||||
.await
|
||||
.context("write chosen files")?;
|
||||
file.flush().await.context("flush chosen file")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -235,6 +287,9 @@ async fn copy(source: &Path, dest: &Path) -> anyhow::Result<()> {
|
||||
|
||||
for entry in walkdir::WalkDir::new(source) {
|
||||
let entry = entry?;
|
||||
|
||||
tracing::debug!("copying path: {}", entry.path().display());
|
||||
|
||||
paths.push(entry.path().strip_prefix(source)?.to_path_buf());
|
||||
}
|
||||
|
||||
@@ -260,10 +315,12 @@ async fn copy_path(src: &Path, dest: &Path) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
if src.is_dir() {
|
||||
tracing::info!("copying dir: {}", dest.display());
|
||||
tokio::fs::create_dir_all(&dest).await.context("copy dir")?;
|
||||
}
|
||||
|
||||
if dest.is_file() {
|
||||
if src.is_file() {
|
||||
tracing::info!("copying file: {}", dest.display());
|
||||
tokio::fs::copy(&src, &dest).await.context("copy file")?;
|
||||
}
|
||||
|
||||
|
@@ -14,7 +14,7 @@ pub enum Action {
|
||||
Edit,
|
||||
}
|
||||
|
||||
pub async fn print_changes(input: &str) -> anyhow::Result<Action> {
|
||||
pub async fn print_changes(input: &str, preview: bool) -> anyhow::Result<Action> {
|
||||
let noil_index = parse_input(input).context("parse input")?;
|
||||
|
||||
fn print_op(key: &str, index: Option<&str>, path: Option<&Path>) {
|
||||
@@ -64,6 +64,11 @@ pub async fn print_changes(input: &str) -> anyhow::Result<Action> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if preview {
|
||||
return Ok(Action::Quit);
|
||||
}
|
||||
|
||||
eprint!("\nApply changes? (y (yes) / n (abort) / E (edit)): ");
|
||||
let mut stderr = std::io::stderr();
|
||||
stderr.flush()?;
|
||||
|
@@ -23,6 +23,9 @@ pub(crate) fn format(input: &str) -> anyhow::Result<String> {
|
||||
| models::Operation::Delete { index }
|
||||
| models::Operation::Move { index }
|
||||
| models::Operation::Existing { index } => index.len(),
|
||||
models::Operation::Open { index } => {
|
||||
index.as_ref().map(|i| i.len()).unwrap_or_default()
|
||||
}
|
||||
models::Operation::Add => 0,
|
||||
})
|
||||
.max()
|
||||
@@ -53,6 +56,7 @@ pub(crate) fn format(input: &str) -> anyhow::Result<String> {
|
||||
| models::Operation::Delete { index }
|
||||
| models::Operation::Move { index }
|
||||
| models::Operation::Existing { index } => Some(index),
|
||||
models::Operation::Open { index } => index,
|
||||
models::Operation::Add => None,
|
||||
};
|
||||
|
||||
|
@@ -35,6 +35,7 @@ pub enum Operation {
|
||||
Copy { index: String },
|
||||
Delete { index: String },
|
||||
Move { index: String },
|
||||
Open { index: Option<String> },
|
||||
}
|
||||
|
||||
impl Display for Operation {
|
||||
@@ -45,6 +46,7 @@ impl Display for Operation {
|
||||
Operation::Copy { .. } => "COPY",
|
||||
Operation::Delete { .. } => "DELETE",
|
||||
Operation::Move { .. } => "MOVE",
|
||||
Operation::Open { .. } => "OPEN",
|
||||
};
|
||||
|
||||
f.write_str(op)
|
||||
@@ -86,6 +88,16 @@ impl FileEntry {
|
||||
"D" | "DEL" | "DELETE" if first != last => Operation::Delete { index },
|
||||
// MOVE:
|
||||
"M" | "MV" | "MOVE" | "RENAME" if first != last => Operation::Move { index },
|
||||
"O" | "OPEN" => Operation::Open {
|
||||
index: {
|
||||
// if LAST == is the Operation, we set the index to empty, if the index is missing we set it to None
|
||||
if index.is_empty() || index.chars().any(|c| c.is_uppercase()) {
|
||||
None
|
||||
} else {
|
||||
Some(index)
|
||||
}
|
||||
},
|
||||
},
|
||||
o => {
|
||||
anyhow::bail!("operation: {} is not supported", o);
|
||||
}
|
||||
@@ -194,6 +206,60 @@ ADD : /var/my/path/new-long-path
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_item_open_operation() -> anyhow::Result<()> {
|
||||
let input = r#"
|
||||
O abc : /var/my
|
||||
OPEN ecd : /var/my/path
|
||||
O : /var/my/path/new-path
|
||||
OPEN : /var/my/path/new-long-path
|
||||
"#;
|
||||
|
||||
let output = parse::parse_input(input)?;
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
Buffer {
|
||||
files: vec![
|
||||
File {
|
||||
path: "/var/my".into(),
|
||||
entry: FileEntry {
|
||||
raw_op: Some("O".into()),
|
||||
operation: Operation::Open {
|
||||
index: Some("abc".into()),
|
||||
}
|
||||
},
|
||||
},
|
||||
File {
|
||||
path: "/var/my/path".into(),
|
||||
entry: FileEntry {
|
||||
raw_op: Some("OPEN".into()),
|
||||
operation: Operation::Open {
|
||||
index: Some("ecd".into())
|
||||
}
|
||||
},
|
||||
},
|
||||
File {
|
||||
path: "/var/my/path/new-path".into(),
|
||||
entry: FileEntry {
|
||||
raw_op: Some("O".into()),
|
||||
operation: Operation::Open { index: None },
|
||||
},
|
||||
},
|
||||
File {
|
||||
path: "/var/my/path/new-long-path".into(),
|
||||
entry: FileEntry {
|
||||
raw_op: Some("OPEN".into()),
|
||||
operation: Operation::Open { index: None },
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
output
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_item_copy_operation() -> anyhow::Result<()> {
|
||||
let input = r#"
|
||||
|
@@ -1,15 +0,0 @@
|
||||
version: "3"
|
||||
services:
|
||||
crdb:
|
||||
restart: 'always'
|
||||
image: 'cockroachdb/cockroach:v23.1.14'
|
||||
command: 'start-single-node --advertise-addr 0.0.0.0 --insecure'
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"]
|
||||
interval: '10s'
|
||||
timeout: '30s'
|
||||
retries: 5
|
||||
start_period: '20s'
|
||||
ports:
|
||||
- 8080:8080
|
||||
- '26257:26257'
|
Reference in New Issue
Block a user