Skip to content
13 changes: 3 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,21 @@

A faster, memory-safe, more ergonomic slopfork of lazygit (🦀 rust btw).

1

This is mostly a "for me" tool — built for my own workflow. Not saying you shouldn't use it, but don't expect it to be a community project. But hey, it works for me!

**Why fork?** PRs were sitting too long, or the upstream direction didn't match how I wanted to work.

2
2.1

The goal: everything lazygit does, but faster and with opinions I actually agree with. (I can't promise backwards-compat w/ lazygit's config since it'll eventually drift w/ my own opinions, but I made sure to do that)

![demo1](https://raw.githubusercontent.com/Blankeos/lazygitrs/main/_docs/demo1.webp)
![demo2](https://raw.githubusercontent.com/Blankeos/lazygitrs/main/_docs/demo2.webp)

3

### Install

> Make sure you have:
> gh
>
> - [git](https://git-scm.com)
> - [gh](https://cli.github.com)

```sh
npm install -g lazygitrs # npm
Expand Down Expand Up @@ -127,5 +122,3 @@ Summary
MIT

Feel free to fork and give it your own spin.
last
1
5 changes: 5 additions & 0 deletions npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ The goal: everything lazygit does, but faster and with opinions I actually agree

### Install

> Make sure you have:
>
> - [git](https://git-scm.com)
> - [gh](https://cli.github.com)

```sh
npm install -g lazygitrs # npm
bun install -g lazygitrs # or bun
Expand Down
6 changes: 6 additions & 0 deletions src/config/keybindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub struct UniversalKeybinding {
pub prev_screen_mode: String,
#[serde(rename = "createPatchOptionsMenu")]
pub create_patch_options_menu: String,
#[serde(rename = "revertBlock")]
pub revert_block: String,
#[serde(rename = "undoRevertBlock")]
pub undo_revert_block: String,
}

impl Default for UniversalKeybinding {
Expand Down Expand Up @@ -157,6 +161,8 @@ impl Default for UniversalKeybinding {
next_screen_mode: "+".into(),
prev_screen_mode: "_".into(),
create_patch_options_menu: "<c-p>".into(),
revert_block: "<enter>".into(),
undo_revert_block: "u".into(),
}
}
}
Expand Down
144 changes: 143 additions & 1 deletion src/git/staging.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result, bail};

use super::GitCommands;

Expand Down Expand Up @@ -77,6 +77,148 @@ impl GitCommands {
};
Ok(self.parse_diff_hunks(&diff))
}

/// Reverse-apply only the lines of a single visual change block to the
/// working tree copy of `file_path`. `want_old` and `want_new` are the
/// inclusive old-file and new-file line-number ranges the visual block
/// covers; either may be `None` for pure insertion or pure deletion.
/// The block typically lives inside one of the `@@` hunks of
/// `unified_diff`, but may be narrower than that `@@` — visual blocks
/// can be split by 1–3 lines of context within a single `@@`.
pub fn revert_visual_block_in_worktree(
&self,
file_path: &str,
unified_diff: &str,
want_old: Option<(usize, usize)>,
want_new: Option<(usize, usize)>,
) -> Result<()> {
let patch = build_visual_block_patch(file_path, unified_diff, want_old, want_new)?;
self.git()
.args(&["apply", "--reverse", "--unidiff-zero", "-"])
.stdin(patch)
.run_expecting_success()
.with_context(|| format!("failed to revert hunk in {}", file_path))?;
Ok(())
}
}

fn build_visual_block_patch(
file_path: &str,
unified_diff: &str,
want_old: Option<(usize, usize)>,
want_new: Option<(usize, usize)>,
) -> Result<String> {
if want_old.is_none() && want_new.is_none() {
bail!("empty visual block");
}

let mut emitted: Vec<String> = Vec::new();
let mut anchor_old: Option<usize> = None;
let mut anchor_new: Option<usize> = None;
let mut old_count = 0usize;
let mut new_count = 0usize;

let mut in_hunk = false;
let mut old_counter = 0usize;
let mut new_counter = 0usize;
let mut last_emitted = false;

for line in unified_diff.lines() {
if line.starts_with("@@") {
let (os, _, ns, _) = parse_hunk_header(line);
old_counter = os;
new_counter = ns;
in_hunk = true;
last_emitted = false;
continue;
}
if !in_hunk {
continue;
}
// A new file's preamble can interleave between hunks of multi-file
// diffs; abandon the current hunk until we see a fresh `@@`.
if line.starts_with("diff ")
|| line.starts_with("--- ")
|| line.starts_with("+++ ")
|| line.starts_with("index ")
|| line.starts_with("similarity ")
|| line.starts_with("rename ")
|| line.starts_with("new file ")
|| line.starts_with("deleted file ")
|| line.starts_with("Binary ")
{
in_hunk = false;
last_emitted = false;
continue;
}
// A "\ No newline at end of file" marker refers to the immediately
// preceding diff line. Propagate it only when that line was emitted.
if line.starts_with('\\') {
if last_emitted {
emitted.push(line.to_string());
}
continue;
}
if line.starts_with('-') {
let in_range =
want_old.is_some_and(|(lo, hi)| old_counter >= lo && old_counter <= hi);
if in_range {
if anchor_old.is_none() {
anchor_old = Some(old_counter);
}
if anchor_new.is_none() {
anchor_new = Some(new_counter);
}
emitted.push(line.to_string());
old_count += 1;
last_emitted = true;
} else {
last_emitted = false;
}
old_counter += 1;
} else if line.starts_with('+') {
let in_range =
want_new.is_some_and(|(lo, hi)| new_counter >= lo && new_counter <= hi);
if in_range {
if anchor_old.is_none() {
anchor_old = Some(old_counter);
}
if anchor_new.is_none() {
anchor_new = Some(new_counter);
}
emitted.push(line.to_string());
new_count += 1;
last_emitted = true;
} else {
last_emitted = false;
}
new_counter += 1;
} else if line.starts_with(' ') || line.is_empty() {
old_counter += 1;
new_counter += 1;
last_emitted = false;
}
}

if emitted.is_empty() {
bail!("visual block matched no diff lines");
}

let old_start = anchor_old.unwrap_or(0);
let new_start = anchor_new.unwrap_or(0);

let mut patch = String::new();
patch.push_str(&format!("--- a/{}\n", file_path));
patch.push_str(&format!("+++ b/{}\n", file_path));
patch.push_str(&format!(
"@@ -{},{} +{},{} @@\n",
old_start, old_count, new_start, new_count
));
for line in &emitted {
patch.push_str(line);
patch.push('\n');
}
Ok(patch)
}

fn parse_hunk_header(header: &str) -> (usize, usize, usize, usize) {
Expand Down
Loading
Loading