8 Commits

Author SHA1 Message Date
f045ac5844 chore(release): v0.1.0 (#9)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.1.0

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #9
2025-08-03 17:01:02 +02:00
f8ba7ee744 feat: add open command
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2025-08-03 16:58:52 +02:00
4bde8ec240 feat: noil now handles open, and open in non-terminals via. /dev/tty
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2025-08-03 16:45:23 +02:00
3fdd9f93a9 feat: add file opener with chooser
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2025-08-03 16:01:59 +02:00
139aa6d75e chore: remove cockroach
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-26 22:35:52 +02:00
a71672823c feat: update demo
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-26 22:16:34 +02:00
c0bd8c0d36 feat: add help text for preview
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-26 22:05:46 +02:00
dadec333d4 chore(release): v0.0.5 (#8)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.0.5

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #8
2025-07-26 21:48:14 +02:00
11 changed files with 385 additions and 55 deletions

View File

@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [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
- actually copy files
## [0.0.4] - 2025-07-26 ## [0.0.4] - 2025-07-26
### Added ### Added

93
Cargo.lock generated
View File

@@ -257,12 +257,44 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 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]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.3" version = "0.3.3"
@@ -345,6 +377,16 @@ version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 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]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.13" version = "0.4.13"
@@ -389,7 +431,7 @@ dependencies = [
[[package]] [[package]]
name = "noil" name = "noil"
version = "0.0.3" version = "0.0.5"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"anyhow", "anyhow",
@@ -399,6 +441,7 @@ dependencies = [
"ignore", "ignore",
"pretty_assertions", "pretty_assertions",
"rand", "rand",
"shellexpand",
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@@ -436,6 +479,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@@ -540,7 +589,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.3.3",
] ]
[[package]] [[package]]
@@ -552,6 +601,17 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.9" version = "0.4.9"
@@ -619,6 +679,15 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shellexpand"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
dependencies = [
"dirs",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@@ -673,6 +742,26 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "thread_local" name = "thread_local"
version = "1.1.9" version = "1.1.9"

View File

@@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.0.4" version = "0.1.0"
[workspace.dependencies] [workspace.dependencies]
noil = { path = "crates/noil" } noil = { path = "crates/noil" }

View File

@@ -1,8 +1,11 @@
# noil # 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.
![demo](assets/demo.gif) ![demo](assets/demo.gif)
@@ -10,17 +13,19 @@ Edit filesystem operations like it's plain text, and then apply them in a contro
## Interactive Mode (TBA) ## 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 ## ✨ Features
* Edit your file tree like a normal buffer - Edit your file tree like a normal buffer
* Preview, format, and apply changes - Preview, format, and apply changes
* Integrates with `$EDITOR` - Integrates with `$EDITOR`
* CLI first, editor agnostic - CLI first, editor agnostic
* No surprises: nothing is applied until you say so - 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 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.
--- ---
@@ -57,12 +62,13 @@ Each line follows this format:
### Supported operations: ### Supported operations:
| Operation | Meaning | Tag Required? | | Operation | Meaning | Tag Required? |
| --------: | --------------------------------- | ------------- | | --------: | ------------------------------------------------------ | ------------- |
| `ADD` | Add new file | ❌ No | | `ADD` | Add new file | ❌ No |
| `COPY` | Copy file with given tag | ✅ Yes | | `COPY` | Copy file with given tag | ✅ Yes |
| `DELETE` | Delete file with given tag | ✅ Yes | | `DELETE` | Delete file with given tag | ✅ Yes |
| `MOVE` | Move file with given tag | ✅ Yes | | `MOVE` | Move file with given tag | ✅ Yes |
| *(blank)* | Reference existing file (default) | ✅ 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 COPY abc : /tmp/nginx-copy
DELETE 123 : /etc/nginx DELETE 123 : /etc/nginx
ADD : /new/file.txt 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 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 ```toml
# .config/helix/languages.toml
[[language]] [[language]]
name = "noil" name = "noil"
scope = "source.noil" scope = "source.noil"
@@ -102,8 +112,30 @@ formatter = { command = "noil", args = ["fmt"] }
[[grammar]] [[grammar]]
name = "noil" name = "noil"
source = { git = "https://git.kjuulh.io/kjuulh/tree-sitter-noil.git", rev = "2f295629439881d0b9e89108a1296881d0daf7b9" } 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 ## 🔒 Safety First
@@ -123,7 +155,9 @@ You will be prompted before anything is modified.
## 🧠 Philosophy ## 🧠 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 cargo install noil
``` ```
**Build from source**: **Build from source**:
```bash ```bash

Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -21,6 +21,7 @@ ignore = "0.4.23"
blake3 = "1.8.2" blake3 = "1.8.2"
rand = "0.9.2" rand = "0.9.2"
ansi_term = "0.12.1" ansi_term = "0.12.1"
shellexpand = "3.1.1"
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"

View File

@@ -1,7 +1,9 @@
use std::path::PathBuf;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use crate::{ use crate::{
cli::edit::apply, cli::edit::{ApplyOptions, apply},
commit::{Action, print_changes}, commit::{Action, print_changes},
}; };
@@ -9,6 +11,9 @@ use crate::{
pub struct ApplyCommand { pub struct ApplyCommand {
#[arg(long = "commit")] #[arg(long = "commit")]
commit: bool, commit: bool,
#[arg(long = "chooser-file", env = "NOIL_CHOOSER_FILE")]
chooser_file: Option<PathBuf>,
} }
impl ApplyCommand { impl ApplyCommand {
@@ -22,13 +27,33 @@ impl ApplyCommand {
if !self.commit { if !self.commit {
let action = print_changes(&input, !self.commit).await?; let action = print_changes(&input, !self.commit).await?;
match action { let res = match action {
Action::Quit => Ok(()), Action::Quit => Ok(()),
Action::Apply { original } => apply(&original).await, Action::Apply { original } => {
Action::Edit => todo!(), 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 { } else {
apply(&input).await apply(
&input,
ApplyOptions {
chooser_file: self.chooser_file.clone(),
..Default::default()
},
)
.await
} }
} }
} }

View File

@@ -1,13 +1,17 @@
use std::{ use std::{
env::temp_dir, env::temp_dir,
io::Write, io::{IsTerminal, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Stdio,
}; };
use ansi_term::Color; use ansi_term::Color;
use anyhow::{Context, bail}; use anyhow::{Context, bail};
use clap::Parser; use clap::Parser;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::{
fs::OpenOptions,
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
};
use crate::{ use crate::{
commit::{Action, print_changes}, commit::{Action, print_changes},
@@ -23,6 +27,15 @@ const PREVIEW: bool = false;
pub struct EditCommand { pub struct EditCommand {
#[arg()] #[arg()]
path: PathBuf, 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 { impl EditCommand {
@@ -49,9 +62,13 @@ impl EditCommand {
.await .await
.context("create temp file for noil")?; .context("create temp file for noil")?;
let output = get_outputs(&self.path, true).await?; let output = get_outputs(&self.get_path().await.context("get path")?, true)
file.write_all(output.as_bytes()).await?; .await
file.flush().await?; .context("get output")?;
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")?; let editor = std::env::var("EDITOR").context("EDITOR not found in env")?;
@@ -59,7 +76,22 @@ impl EditCommand {
let mut cmd = tokio::process::Command::new(editor.trim()); let mut cmd = tokio::process::Command::new(editor.trim());
cmd.arg(&file_path); 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")?; let status = process.wait().await.context("editor closed prematurely")?;
if !status.success() { if !status.success() {
let code = status.code().unwrap_or(-1); let code = status.code().unwrap_or(-1);
@@ -70,7 +102,13 @@ impl EditCommand {
.await .await
.context("read noil file")?; .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 { let action = match res {
Ok(a) => a, Ok(a) => a,
@@ -80,7 +118,7 @@ impl EditCommand {
Color::Red.normal().paint(format!("{e:?}")) Color::Red.normal().paint(format!("{e:?}"))
); );
wait_user().await?; wait_user().await.context("user finished prematurely")?;
continue; continue;
} }
@@ -89,12 +127,38 @@ impl EditCommand {
match action { match action {
Action::Quit => return Ok(()), Action::Quit => return Ok(()),
Action::Apply { original } => { Action::Apply { original } => {
return apply(&original).await; return apply(
&original,
ApplyOptions {
chooser_file: self.chooser_file.clone(),
quiet: self.quiet,
},
)
.await;
} }
Action::Edit => continue, 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() {
return path
.parent()
.map(|p| p.to_path_buf())
.ok_or(anyhow::anyhow!("parent doesn't exist for file"));
}
Ok(path.clone())
}
} }
async fn wait_user() -> Result<(), anyhow::Error> { async fn wait_user() -> Result<(), anyhow::Error> {
@@ -103,10 +167,19 @@ async fn wait_user() -> Result<(), anyhow::Error> {
let stdin = tokio::io::stdin(); let stdin = tokio::io::stdin();
let mut reader = BufReader::new(stdin); let mut reader = BufReader::new(stdin);
let mut input_buf = String::new(); 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(()) 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. /// the philosphy behind apply is that we try unlike normal file system operations to be idempotent.
/// This is mainly for 2 reasons. /// This is mainly for 2 reasons.
/// ///
@@ -114,17 +187,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 /// 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 /// 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<()> {
if !options.quiet {
eprintln!("applying changes"); eprintln!("applying changes");
}
let noil_index = parse::parse_input(input).context("parse input")?; let noil_index = parse::parse_input(input).context("parse input")?;
let mut open_files = Vec::new();
for file in &noil_index.files { for file in &noil_index.files {
let path = &file.path; let path = &file.path;
match &file.entry.operation { match &file.entry.operation {
Operation::Existing { .. } => { Operation::Existing { .. } => {
// Noop // 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 => { Operation::Add => {
tracing::debug!("creating file"); tracing::debug!("creating file");
@@ -230,6 +315,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(()) Ok(())
} }

View File

@@ -23,6 +23,9 @@ pub(crate) fn format(input: &str) -> anyhow::Result<String> {
| models::Operation::Delete { index } | models::Operation::Delete { index }
| models::Operation::Move { index } | models::Operation::Move { index }
| models::Operation::Existing { index } => index.len(), | models::Operation::Existing { index } => index.len(),
models::Operation::Open { index } => {
index.as_ref().map(|i| i.len()).unwrap_or_default()
}
models::Operation::Add => 0, models::Operation::Add => 0,
}) })
.max() .max()
@@ -53,6 +56,7 @@ pub(crate) fn format(input: &str) -> anyhow::Result<String> {
| models::Operation::Delete { index } | models::Operation::Delete { index }
| models::Operation::Move { index } | models::Operation::Move { index }
| models::Operation::Existing { index } => Some(index), | models::Operation::Existing { index } => Some(index),
models::Operation::Open { index } => index,
models::Operation::Add => None, models::Operation::Add => None,
}; };

View File

@@ -35,6 +35,7 @@ pub enum Operation {
Copy { index: String }, Copy { index: String },
Delete { index: String }, Delete { index: String },
Move { index: String }, Move { index: String },
Open { index: Option<String> },
} }
impl Display for Operation { impl Display for Operation {
@@ -45,6 +46,7 @@ impl Display for Operation {
Operation::Copy { .. } => "COPY", Operation::Copy { .. } => "COPY",
Operation::Delete { .. } => "DELETE", Operation::Delete { .. } => "DELETE",
Operation::Move { .. } => "MOVE", Operation::Move { .. } => "MOVE",
Operation::Open { .. } => "OPEN",
}; };
f.write_str(op) f.write_str(op)
@@ -86,6 +88,16 @@ impl FileEntry {
"D" | "DEL" | "DELETE" if first != last => Operation::Delete { index }, "D" | "DEL" | "DELETE" if first != last => Operation::Delete { index },
// MOVE: // MOVE:
"M" | "MV" | "MOVE" | "RENAME" if first != last => Operation::Move { index }, "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 => { o => {
anyhow::bail!("operation: {} is not supported", o); anyhow::bail!("operation: {} is not supported", o);
} }
@@ -194,6 +206,60 @@ ADD : /var/my/path/new-long-path
Ok(()) 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] #[test]
fn can_parse_item_copy_operation() -> anyhow::Result<()> { fn can_parse_item_copy_operation() -> anyhow::Result<()> {
let input = r#" let input = r#"

View File

@@ -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'