Skip to content

Commit

Permalink
Initial version '0.1.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenengler committed Feb 4, 2024
1 parent 84fa321 commit 1decf69
Show file tree
Hide file tree
Showing 11 changed files with 2,213 additions and 1 deletion.
652 changes: 652 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,21 @@
name = "repatch"
version = "0.1.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/stevenengler/repatch"
description = "A regex find-and-replace tool with a `git add --patch`-like interface."
keywords = ["search", "find", "replace", "regex", "patch"]
categories = ["command-line-utilities", "text processing", "filesystem"]

[dependencies]
anstyle = "1.0.4"
anyhow = "1.0.79"
bstr = { version = "1.9.0", features = ["unicode"] }
clap = { version = "4.4.16", features = ["derive", "wrap_help"] }
diffy = "0.3.0"
grep-matcher = "0.1.7"
grep-regex = "0.1.12"
grep-searcher = "0.1.13"
ignore = "0.4.22"
libc = "0.2.152"
tempfile = "3.9.0"
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# re:patch

[![Latest Version]][crates.io]

re:patch is a line-oriented find-and-replace tool with a [`git add
--patch`][git-add-patch]-like interface. Regular expressions and capture groups
are supported, and re:patch can be used with files and directories. Only Linux
is currently supported.

> [!WARNING]
> This tool is still in development. While it Works For Me™, it does not yet
> have any tests. It's recommended to only use this in directories that are
> version controlled.
[crates.io]: https://crates.io/crates/repatch
[Latest Version]: https://img.shields.io/crates/v/repatch.svg
[git-add-patch]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch

### Install

You can install from source or through crates.io. You must have a recent
[rust/cargo][rust] toolchain installed.

```
# install the latest release from crates.io
cargo install repatch
# install from source
git clone https://github.com/stevenengler/repatch.git
cd repatch && cargo install --path .
```

[rust]: https://www.rust-lang.org/tools/install

### Example

<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/example-dark.png">
<img width="730" alt="Command-line usage example." src="docs/assets/example-light.png">
</picture>

### Notes

Similar to [ripgrep][ripgrep], gitignore rules are respected and hidden
files/directories are ignored.

The editor used to edit patches can be configured using environment variables
or the git configuration. The search priority is `VISUAL`, `EDITOR`,
`GIT_EDITOR`, and `git config core.editor`. Otherwise vim is used. Like `sudo
-e` the editor value is split by whitespace characters and executed, and is not
interpreted by a shell.

Patches shown in the terminal will have ANSI escape sequences replaced with
safe versions.

Like most text editors, files are replaced and not edited in-place. This means
that the file owner or other metadata may change after editing. The new file
will have the same read/write/execute permissions as the original file. You
will also need enough temporary disk space for this second file. For example if
you're editing a 10 GB file, you must have at least 10 GB of disk space free so
that the new file can be written before the original file is deleted.

Large files (larger than the amount of available memory) are supported as long
as they have a sufficient number of lines. For example a 10 GB file with 10,000
lines should work fine, but a 10 GB file with a single line might exhaust the
system memory and would not look very nice in the terminal.

[ripgrep]: https://github.com/BurntSushi/ripgrep

### Acknowledgements

Most of the heavy lifting is done by the [ripgrep][ripgrep] family of crates,
[clap][clap], and [diffy][diffy].

[clap]: https://docs.rs/clap/latest/clap/
[diffy]: https://docs.rs/diffy/latest/diffy/
Binary file added docs/assets/example-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/example-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions docs/publishing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Publishing a new release

1. Update the code.

```bash
# make sure we don't include personal information (such as our
# home directory name) in the release
cd /tmp

# make sure we don't include any untracked files in the release
git clone [email protected]:stevenengler/repatch.git
cd repatch

# update the version
vim Cargo.toml
cargo update --package repatch

# check for errors
git diff
cargo publish --dry-run --allow-dirty

# add and commit version changes with commit message, for example
# "Updated version to '0.2.1'"
git add --patch
git commit
git push
```

2. After CI tests finish on GitHub, mark it as a new release.

3. Publish the crate.

```bash
# make sure there are no untracked or changed files
git status
# publish
cargo publish --dry-run
cargo publish
```
72 changes: 72 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::path::PathBuf;

use clap::Parser;

const VERSION_STR: &str = concat!("re:patch ", env!("CARGO_PKG_VERSION"));

/// re:patch is a line-oriented find-and-replace tool with a `git add --patch`-like interface.
/// Directories are searched recursively. Hidden files/directories and binary files are ignored, as
/// well as files/directories specified in gitignore rules. Regular expressions with capture groups
/// are supported.
#[derive(Debug, Parser)]
#[command(version, name = "re:patch", max_term_width = 120, help_expected = true)]
#[command(before_help(VERSION_STR))]
pub struct Args {
/// Regex to search for, optionally with capture groups.
pub find: String,
/// Text to replace `<FIND>` with. Capture group indices and names are supported.
pub replace: String,
/// Paths (files and/or directories) to search recursively.
#[clap(required = true)]
pub paths: Vec<PathBuf>,
/// Case-insensitive search.
#[clap(long, short)]
pub ignore_case: bool,
/// Ignore filesystem-related errors while searching ("no such file", "permission denied", etc).
#[clap(long)]
pub ignore_errors: bool,
/// Generate diffs with `<N>` lines of context; also accepts "infinite".
#[clap(long, default_value_t, value_name = "N")]
pub context: Context,
/// Show the changes without modifying any files.
///
/// This does not generate valid patch files and is meant only for terminal output. ANSI escape
/// sequences are replaced in the generated patches.
#[clap(long, conflicts_with_all(["apply"]))]
pub show: bool,
/// Apply and write all changes automatically without any user input or confirmation.
#[clap(long)]
pub apply: bool,
}

#[derive(Copy, Clone, Debug)]
pub enum Context {
Num(u64),
Infinite,
}

impl std::str::FromStr for Context {
type Err = std::num::ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"infinite" => Self::Infinite,
x => Self::Num(x.parse()?),
})
}
}

impl std::fmt::Display for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Num(x) => write!(f, "{x}"),
Self::Infinite => write!(f, "infinite"),
}
}
}

impl Default for Context {
fn default() -> Self {
Self::Num(5)
}
}
Loading

0 comments on commit 1decf69

Please sign in to comment.