Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
d1b20ee5aa | |||
da00f41205 | |||
f045ac5844 | |||
f8ba7ee744
|
|||
4bde8ec240
|
|||
3fdd9f93a9
|
|||
139aa6d75e
|
|||
a71672823c
|
|||
c0bd8c0d36
|
17
CHANGELOG.md
17
CHANGELOG.md
@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.1] - 2025-08-04
|
||||
|
||||
### Fixed
|
||||
- if path is empty default to `.`
|
||||
|
||||
## [0.1.0] - 2025-08-03
|
||||
|
||||
### Added
|
||||
- add open command
|
||||
- noil now handles open, and open in non-terminals via. /dev/tty
|
||||
- add file opener with chooser
|
||||
- update demo
|
||||
- add help text for preview
|
||||
|
||||
### Other
|
||||
- remove cockroach
|
||||
|
||||
## [0.0.5] - 2025-07-26
|
||||
|
||||
### Added
|
||||
|
93
Cargo.lock
generated
93
Cargo.lock
generated
@@ -257,12 +257,44 @@ version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
@@ -345,6 +377,16 @@ version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
@@ -389,7 +431,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "noil"
|
||||
version = "0.0.3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -399,6 +441,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"pretty_assertions",
|
||||
"rand",
|
||||
"shellexpand",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -436,6 +479,12 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -540,7 +589,7 @@ version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -552,6 +601,17 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
@@ -619,6 +679,15 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -673,6 +742,26 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
|
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.5"
|
||||
version = "0.1.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
noil = { path = "crates/noil" }
|
||||
|
73
README.md
73
README.md
@@ -1,8 +1,11 @@
|
||||
# noil
|
||||
|
||||
**noil** is a structured, text-buffer-based file operation tool – think of it like [`oil.nvim`](https://github.com/stevearc/oil.nvim), but for any editor, terminal, or automated process.
|
||||
**noil** is a structured, text-buffer-based file operation tool – think of it
|
||||
like [`oil.nvim`](https://github.com/stevearc/oil.nvim), but for any editor,
|
||||
terminal, or automated process.
|
||||
|
||||
Edit filesystem operations like it's plain text, and then apply them in a controlled, explicit way.
|
||||
Edit filesystem operations like it's plain text, and then apply them in a
|
||||
controlled, explicit way.
|
||||
|
||||

|
||||
|
||||
@@ -10,17 +13,19 @@ Edit filesystem operations like it's plain text, and then apply them in a contro
|
||||
|
||||
## Interactive Mode (TBA)
|
||||
|
||||
I am planning an interactive TUI mode, where you don't have to care about tags, like in `oil`. For now the normal editor is quite useful though, and allows all types of editors to easily move, edit files and so on.
|
||||
I am planning an interactive TUI mode, where you don't have to care about tags,
|
||||
like in `oil`. For now the normal editor is quite useful though, and allows all
|
||||
types of editors to easily move, edit files and so on.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Edit your file tree like a normal buffer
|
||||
* Preview, format, and apply changes
|
||||
* Integrates with `$EDITOR`
|
||||
* CLI first, editor agnostic
|
||||
* No surprises: nothing is applied until you say so
|
||||
- Edit your file tree like a normal buffer
|
||||
- Preview, format, and apply changes
|
||||
- Integrates with `$EDITOR`
|
||||
- CLI first, editor agnostic
|
||||
- No surprises: nothing is applied until you say so
|
||||
|
||||
---
|
||||
|
||||
@@ -42,7 +47,7 @@ cat something.noil | noil fmt > something.noil
|
||||
cat something.noil | noil apply
|
||||
```
|
||||
|
||||
noil will always ask you if you want to apply your changes before doing any operations.
|
||||
noil will ask you if you want to apply your changes before doing any operations.
|
||||
|
||||
---
|
||||
|
||||
@@ -56,13 +61,14 @@ Each line follows this format:
|
||||
|
||||
### Supported operations:
|
||||
|
||||
| Operation | Meaning | Tag Required? |
|
||||
| --------: | --------------------------------- | ------------- |
|
||||
| `ADD` | Add new file | ❌ No |
|
||||
| `COPY` | Copy file with given tag | ✅ Yes |
|
||||
| `DELETE` | Delete file with given tag | ✅ Yes |
|
||||
| `MOVE` | Move file with given tag | ✅ Yes |
|
||||
| *(blank)* | Reference existing file (default) | ✅ Yes |
|
||||
| Operation | Meaning | Tag Required? |
|
||||
| --------: | ------------------------------------------------------ | ------------- |
|
||||
| `ADD` | Add new file | ❌ No |
|
||||
| `COPY` | Copy file with given tag | ✅ Yes |
|
||||
| `DELETE` | Delete file with given tag | ✅ Yes |
|
||||
| `MOVE` | Move file with given tag | ✅ Yes |
|
||||
| `OPEN` | Open a file with a given tag (requires --chooser-file) | ❌ No |
|
||||
| _(blank)_ | Reference existing file (default) | ✅ Yes |
|
||||
|
||||
---
|
||||
|
||||
@@ -73,9 +79,11 @@ Each line follows this format:
|
||||
COPY abc : /tmp/nginx-copy
|
||||
DELETE 123 : /etc/nginx
|
||||
ADD : /new/file.txt
|
||||
OPEN : /new/file.txt
|
||||
```
|
||||
|
||||
You can use short, unique tags (like `abc`, `ng1`, etc.) to refer to files. `noil` will generate these tags when you run `noil .`.
|
||||
You can use short, unique tags (like `abc`, `ng1`, etc.) to refer to files.
|
||||
`noil` will generate these tags when you run `noil .`.
|
||||
|
||||
---
|
||||
|
||||
@@ -87,9 +95,11 @@ Want to clean up alignment and spacing?
|
||||
cat my-buffer.noil | noil fmt
|
||||
```
|
||||
|
||||
Or automatically format inside your editor with the following config for [Helix](https://helix-editor.com):
|
||||
Or automatically format inside your editor with the following config for
|
||||
[Helix](https://helix-editor.com):
|
||||
|
||||
```toml
|
||||
# .config/helix/languages.toml
|
||||
[[language]]
|
||||
name = "noil"
|
||||
scope = "source.noil"
|
||||
@@ -102,8 +112,30 @@ formatter = { command = "noil", args = ["fmt"] }
|
||||
[[grammar]]
|
||||
name = "noil"
|
||||
source = { git = "https://git.kjuulh.io/kjuulh/tree-sitter-noil.git", rev = "2f295629439881d0b9e89108a1296881d0daf7b9" }
|
||||
|
||||
# .config/helix/config.toml
|
||||
# Optional extra command Space + o will open noil allowing edits and the OPEN command
|
||||
[keys.normal.space]
|
||||
o = [
|
||||
":sh rm -f /tmp/unique-file-kjuulh",
|
||||
# DISCLAIMER: Until noil has a proper interactive mode, we cannot ask for confirmation, as such we always commit changes, you don't get to have a preview unlike the normal cli option
|
||||
":insert-output noil edit '%{buffer_name}' --chooser-file=/tmp/unique-file-kjuulh --commit --quiet < /dev/tty",
|
||||
":insert-output echo \"x1b[?1049h\" > /dev/tty",
|
||||
":open %sh{cat /tmp/unique-file-kjuulh}",
|
||||
":redraw",
|
||||
]
|
||||
```
|
||||
|
||||
### Edit options
|
||||
|
||||
When using `noil edit .` a few additional options are available
|
||||
|
||||
- `--chooser-file`: A chooser file is a newline delimited file where each line
|
||||
corresponds to a relative file to be opened or manipulated by the user. Only
|
||||
items with `OPEN` command will be added to the file
|
||||
- `--commit`: commit files without asking for confirmation
|
||||
- `--quiet`: don't print results
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Safety First
|
||||
@@ -123,7 +155,9 @@ You will be prompted before anything is modified.
|
||||
|
||||
## 🧠 Philosophy
|
||||
|
||||
noil gives you full control over file operations in a composable and editor-friendly way. Think Git index, but for actual file moves and deletions — human-editable, patchable, and grep-able.
|
||||
noil gives you full control over file operations in a composable and
|
||||
editor-friendly way. Think Git index, but for actual file moves and deletions —
|
||||
human-editable, patchable, and grep-able.
|
||||
|
||||
---
|
||||
|
||||
@@ -135,7 +169,6 @@ noil gives you full control over file operations in a composable and editor-frie
|
||||
cargo install noil
|
||||
```
|
||||
|
||||
|
||||
**Build from source**:
|
||||
|
||||
```bash
|
||||
|
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 |
@@ -21,6 +21,7 @@ ignore = "0.4.23"
|
||||
blake3 = "1.8.2"
|
||||
rand = "0.9.2"
|
||||
ansi_term = "0.12.1"
|
||||
shellexpand = "3.1.1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
|
@@ -1,7 +1,9 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::{
|
||||
cli::edit::apply,
|
||||
cli::edit::{ApplyOptions, apply},
|
||||
commit::{Action, print_changes},
|
||||
};
|
||||
|
||||
@@ -9,6 +11,9 @@ use crate::{
|
||||
pub struct ApplyCommand {
|
||||
#[arg(long = "commit")]
|
||||
commit: bool,
|
||||
|
||||
#[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")]
|
||||
chooser_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ApplyCommand {
|
||||
@@ -22,13 +27,33 @@ impl ApplyCommand {
|
||||
|
||||
if !self.commit {
|
||||
let action = print_changes(&input, !self.commit).await?;
|
||||
match action {
|
||||
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(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
Action::Edit => todo!(),
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!("\nin preview mode: add (--commit) to perform actions");
|
||||
|
||||
res
|
||||
} else {
|
||||
apply(&input).await
|
||||
apply(
|
||||
&input,
|
||||
ApplyOptions {
|
||||
chooser_file: self.chooser_file.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,17 @@
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
io::Write,
|
||||
io::{IsTerminal, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
use ansi_term::Color;
|
||||
use anyhow::{Context, bail};
|
||||
use clap::Parser;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::{
|
||||
fs::OpenOptions,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
commit::{Action, print_changes},
|
||||
@@ -23,6 +27,15 @@ const PREVIEW: bool = false;
|
||||
pub struct EditCommand {
|
||||
#[arg()]
|
||||
path: PathBuf,
|
||||
|
||||
#[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")]
|
||||
chooser_file: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "commit")]
|
||||
commit: bool,
|
||||
|
||||
#[arg(long = "quiet")]
|
||||
quiet: bool,
|
||||
}
|
||||
|
||||
impl EditCommand {
|
||||
@@ -49,9 +62,14 @@ impl EditCommand {
|
||||
.await
|
||||
.context("create temp file for noil")?;
|
||||
|
||||
let output = get_outputs(&self.path, true).await?;
|
||||
file.write_all(output.as_bytes()).await?;
|
||||
file.flush().await?;
|
||||
let path = &self.get_path().await.context("get path")?;
|
||||
let output = get_outputs(path, true)
|
||||
.await
|
||||
.context(format!("get output: {}", path.display()))?;
|
||||
file.write_all(output.as_bytes())
|
||||
.await
|
||||
.context("write contents for edit")?;
|
||||
file.flush().await.context("flush contents for edit")?;
|
||||
|
||||
let editor = std::env::var("EDITOR").context("EDITOR not found in env")?;
|
||||
|
||||
@@ -59,7 +77,22 @@ impl EditCommand {
|
||||
let mut cmd = tokio::process::Command::new(editor.trim());
|
||||
cmd.arg(&file_path);
|
||||
|
||||
let mut process = cmd.spawn()?;
|
||||
if !std::io::stdout().is_terminal() {
|
||||
let tty = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/tty")
|
||||
.await
|
||||
.context("open tty")?;
|
||||
let tty_in = tty.try_clone().await.context("clone ttyin")?;
|
||||
let tty_out = tty.try_clone().await.context("clone ttyout")?;
|
||||
|
||||
cmd.stdin(Stdio::from(tty_in.into_std().await))
|
||||
.stdout(Stdio::from(tty_out.into_std().await))
|
||||
.stderr(Stdio::from(tty.into_std().await));
|
||||
}
|
||||
|
||||
let mut process = cmd.spawn().context("command not found")?;
|
||||
let status = process.wait().await.context("editor closed prematurely")?;
|
||||
if !status.success() {
|
||||
let code = status.code().unwrap_or(-1);
|
||||
@@ -70,7 +103,13 @@ impl EditCommand {
|
||||
.await
|
||||
.context("read noil file")?;
|
||||
|
||||
let res = print_changes(&noil_content, PREVIEW).await;
|
||||
let res = if !self.commit {
|
||||
print_changes(&noil_content, PREVIEW).await
|
||||
} else {
|
||||
Ok(Action::Apply {
|
||||
original: noil_content,
|
||||
})
|
||||
};
|
||||
|
||||
let action = match res {
|
||||
Ok(a) => a,
|
||||
@@ -80,7 +119,7 @@ impl EditCommand {
|
||||
Color::Red.normal().paint(format!("{e:?}"))
|
||||
);
|
||||
|
||||
wait_user().await?;
|
||||
wait_user().await.context("user finished prematurely")?;
|
||||
|
||||
continue;
|
||||
}
|
||||
@@ -89,12 +128,44 @@ 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(),
|
||||
quiet: self.quiet,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Action::Edit => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_path(&self) -> anyhow::Result<PathBuf> {
|
||||
let path_str = self.path.display().to_string();
|
||||
let expanded_path = shellexpand::full(&path_str)?;
|
||||
let path = PathBuf::from(expanded_path.to_string());
|
||||
|
||||
if !path.exists() {
|
||||
anyhow::bail!("path: {} does not exist", self.path.display());
|
||||
}
|
||||
|
||||
if path.is_file() {
|
||||
let parent_path = path
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.ok_or(anyhow::anyhow!("parent doesn't exist for file"))?;
|
||||
|
||||
if parent_path.display().to_string() == "" {
|
||||
return Ok(PathBuf::from("."));
|
||||
}
|
||||
|
||||
return Ok(parent_path);
|
||||
}
|
||||
|
||||
Ok(path.clone())
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_user() -> Result<(), anyhow::Error> {
|
||||
@@ -103,10 +174,19 @@ async fn wait_user() -> Result<(), anyhow::Error> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut reader = BufReader::new(stdin);
|
||||
let mut input_buf = String::new();
|
||||
reader.read_line(&mut input_buf).await?;
|
||||
reader
|
||||
.read_line(&mut input_buf)
|
||||
.await
|
||||
.context("failed to read stdin")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ApplyOptions {
|
||||
pub chooser_file: Option<PathBuf>,
|
||||
pub quiet: bool,
|
||||
}
|
||||
|
||||
/// the philosphy behind apply is that we try unlike normal file system operations to be idempotent.
|
||||
/// This is mainly for 2 reasons.
|
||||
///
|
||||
@@ -114,17 +194,29 @@ 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<()> {
|
||||
eprintln!("applying changes");
|
||||
pub async fn apply(input: &str, options: ApplyOptions) -> anyhow::Result<()> {
|
||||
if !options.quiet {
|
||||
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");
|
||||
|
||||
@@ -230,6 +322,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(())
|
||||
}
|
||||
|
||||
|
@@ -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