diff --git a/src/default_config.toml b/src/default_config.toml index bacc6af4f9..a2aee60fb7 100644 --- a/src/default_config.toml +++ b/src/default_config.toml @@ -119,6 +119,7 @@ root.discard = ["K"] root.stage = ["s"] root.unstage = ["u"] root.apply = ["a"] +root.reverse = ["v"] root.copy_hash = ["y"] picker.next = ["down", "ctrl+n", "tab"] diff --git a/src/ops/mod.rs b/src/ops/mod.rs index d3269844d6..1f392a0a2e 100644 --- a/src/ops/mod.rs +++ b/src/ops/mod.rs @@ -24,6 +24,7 @@ pub(crate) mod push; pub(crate) mod rebase; pub(crate) mod remote; pub(crate) mod reset; +pub(crate) mod reverse; pub(crate) mod revert; pub(crate) mod show; pub(crate) mod show_refs; @@ -107,6 +108,7 @@ pub(crate) enum Op { Show, Discard, Apply, + Reverse, CopyHash, ToggleSection, @@ -199,6 +201,7 @@ impl Op { Op::Stage => Box::new(stage::Stage), Op::Unstage => Box::new(unstage::Unstage), Op::Apply => Box::new(apply::Apply), + Op::Reverse => Box::new(reverse::Reverse), Op::CopyHash => Box::new(copy_hash::CopyHash), Op::AddRemote => Box::new(remote::AddRemote), diff --git a/src/ops/reverse.rs b/src/ops/reverse.rs new file mode 100644 index 0000000000..e77c2ff102 --- /dev/null +++ b/src/ops/reverse.rs @@ -0,0 +1,63 @@ +use super::OpTrait; +use crate::{ + Action, + app::{App, State}, + git::diff::{Diff, PatchMode}, + item_data::ItemData, + term::Term, +}; +use std::{process::Command, rc::Rc}; + +pub(crate) struct Reverse; +impl OpTrait for Reverse { + fn get_action(&self, target: &ItemData) -> Option { + let action = match target { + ItemData::Delta { diff, file_i } => reverse_patch(diff.format_file_patch(*file_i)), + ItemData::Hunk { + diff, + file_i, + hunk_i, + } => reverse_patch(diff.format_hunk_patch(*file_i, *hunk_i)), + ItemData::HunkLine { + diff, + file_i, + hunk_i, + line_i, + .. + } => reverse_line(diff, *file_i, *hunk_i, *line_i), + _ => return None, + }; + + Some(action) + } + + fn is_target_op(&self) -> bool { + true + } + + fn display(&self, _state: &State) -> String { + "Reverse".into() + } +} + +fn reverse_patch(patch: String) -> Action { + let patch = patch.into_bytes(); + + Rc::new(move |app: &mut App, term: &mut Term| { + let mut cmd = Command::new("git"); + cmd.args(["apply", "--reverse"]); + app.run_cmd(term, &patch, cmd) + }) +} + +fn reverse_line(diff: &Rc, file_i: usize, hunk_i: usize, line_i: usize) -> Action { + let patch = diff + .format_line_patch(file_i, hunk_i, line_i..(line_i + 1), PatchMode::Reverse) + .into_bytes(); + + Rc::new(move |app: &mut App, term: &mut Term| { + let mut cmd = Command::new("git"); + cmd.args(["apply", "--reverse", "--recount"]); + app.run_cmd(term, &patch, cmd) + }) +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 40ce76be58..9df83e6eb1 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -28,6 +28,7 @@ mod quit; mod rebase; mod remote; mod reset; +mod reverse; mod stage; mod stash; mod unstage; diff --git a/src/tests/reverse.rs b/src/tests/reverse.rs new file mode 100644 index 0000000000..ee38340cd8 --- /dev/null +++ b/src/tests/reverse.rs @@ -0,0 +1,73 @@ +use super::*; + +fn snapshot_with_file(snapshot_name: &str, mut ctx: TestContext, filename: &str, keys_input: &str) { + let before = fs::read_to_string(ctx.dir.join(filename)).unwrap(); + + let mut app = ctx.init_app(); + ctx.update(&mut app, keys(keys_input)); + + let after = fs::read_to_string(ctx.dir.join(filename)).unwrap(); + + let mut out = ctx.redact_buffer(); + out.push_str("\n\n[file before]\n"); + out.push_str(&before); + out.push_str("\n[file after]\n"); + out.push_str(&after); + + insta::assert_snapshot!(snapshot_name, out); +} + +fn setup(ctx: TestContext) -> TestContext { + commit(&ctx.dir, "file-one", "FOO\nBAR\nBAZ\n"); + fs::write(ctx.dir.join("file-one"), "blahonga\nBAR\nBAZ\n").unwrap(); + ctx +} + +#[test] +pub(crate) fn reverse_unstaged_delta() { + let ctx = setup(setup_clone!()); + let snapshot_name = function_name!().rsplit("::").next().unwrap(); + snapshot_with_file(snapshot_name, ctx, "file-one", "jjv"); +} + +#[test] +pub(crate) fn reverse_unstaged_hunk() { + let ctx = setup(setup_clone!()); + let snapshot_name = function_name!().rsplit("::").next().unwrap(); + snapshot_with_file(snapshot_name, ctx, "file-one", "jjjv"); +} + +#[test] +pub(crate) fn reverse_unstaged_line() { + let ctx = setup(setup_clone!()); + let snapshot_name = function_name!().rsplit("::").next().unwrap(); + snapshot_with_file(snapshot_name, ctx, "file-one", "jjjv"); +} + +#[test] +pub(crate) fn reverse_staged_delta() { + let ctx = setup_staged(setup_clone!()); + let snapshot_name = function_name!().rsplit("::").next().unwrap(); + snapshot_with_file(snapshot_name, ctx, "file-one", "jjv"); +} + +#[test] +pub(crate) fn reverse_staged_hunk() { + let ctx = setup_staged(setup_clone!()); + let snapshot_name = function_name!().rsplit("::").next().unwrap(); + snapshot_with_file(snapshot_name, ctx, "file-one", "jjjv"); +} + +#[test] +pub(crate) fn reverse_staged_line() { + let ctx = setup_staged(setup_clone!()); + let snapshot_name = function_name!().rsplit("::").next().unwrap(); + snapshot_with_file(snapshot_name, ctx, "file-one", "jjjv"); +} + +fn setup_staged(ctx: TestContext) -> TestContext { + commit(&ctx.dir, "file-one", "FOO\nBAR\nBAZ\n"); + fs::write(ctx.dir.join("file-one"), "blahonga\nBAR\nBAZ\n").unwrap(); + run(&ctx.dir, &["git", "add", "file-one"]); + ctx +} diff --git a/src/tests/snapshots/gitu__tests__reverse__reverse_staged_delta.snap b/src/tests/snapshots/gitu__tests__reverse__reverse_staged_delta.snap new file mode 100644 index 0000000000..e9ee8c69ab --- /dev/null +++ b/src/tests/snapshots/gitu__tests__reverse__reverse_staged_delta.snap @@ -0,0 +1,35 @@ +--- +source: src/tests/reverse.rs +expression: out +--- + On branch main | + Your branch is ahead of 'origin/main' by 1 commit(s). | + | + Unstaged changes (1) | +▌modified file-one | +▌@@ -1,3 +1,3 @@ | +▌-blahonga | +▌+FOO | +▌ BAR | +▌ BAZ | + | + Staged changes (1) | + modified file-one… | + | + Recent commits | + 9f4a45e main add file-one | + b66a0bf origin/main add initial-file | + | +────────────────────────────────────────────────────────────────────────────────| +$ git apply --reverse | +styles_hash: c3e27c3c64b57280 + +[file before] +blahonga +BAR +BAZ + +[file after] +FOO +BAR +BAZ diff --git a/src/tests/snapshots/gitu__tests__reverse__reverse_staged_hunk.snap b/src/tests/snapshots/gitu__tests__reverse__reverse_staged_hunk.snap new file mode 100644 index 0000000000..6b41ca2121 --- /dev/null +++ b/src/tests/snapshots/gitu__tests__reverse__reverse_staged_hunk.snap @@ -0,0 +1,35 @@ +--- +source: src/tests/reverse.rs +expression: out +--- + On branch main | + Your branch is ahead of 'origin/main' by 1 commit(s). | + | + Unstaged changes (1) | + modified file-one | +▌@@ -1,3 +1,3 @@ | +▌-blahonga | +▌+FOO | +▌ BAR | +▌ BAZ | + | + Staged changes (1) | + modified file-one | + @@ -1,3 +1,3 @@ | + -FOO | + +blahonga | + BAR | + BAZ | +────────────────────────────────────────────────────────────────────────────────| +$ git apply --reverse | +styles_hash: a5e24bbded675128 + +[file before] +blahonga +BAR +BAZ + +[file after] +FOO +BAR +BAZ diff --git a/src/tests/snapshots/gitu__tests__reverse__reverse_staged_line.snap b/src/tests/snapshots/gitu__tests__reverse__reverse_staged_line.snap new file mode 100644 index 0000000000..02544defa9 --- /dev/null +++ b/src/tests/snapshots/gitu__tests__reverse__reverse_staged_line.snap @@ -0,0 +1,36 @@ +--- +source: src/tests/reverse.rs +expression: out +--- + On branch main | + Your branch is ahead of 'origin/main' by 1 commit(s). | + | + Unstaged changes (1) | + modified file-one | + @@ -1,3 +1,4 @@ | +▌+FOO | + blahonga | + BAR | + BAZ | + | + Staged changes (1) | + modified file-one | + @@ -1,3 +1,3 @@ | + -FOO | + +blahonga | + BAR | + BAZ | +────────────────────────────────────────────────────────────────────────────────| +$ git apply --reverse --recount | +styles_hash: d8e20f905aae1d59 + +[file before] +blahonga +BAR +BAZ + +[file after] +FOO +blahonga +BAR +BAZ diff --git a/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_delta.snap b/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_delta.snap new file mode 100644 index 0000000000..5b2d5eca6c --- /dev/null +++ b/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_delta.snap @@ -0,0 +1,35 @@ +--- +source: src/tests/reverse.rs +expression: out +--- + On branch main | + Your branch is ahead of 'origin/main' by 1 commit(s). | + | + Recent commits | +▌9f4a45e main add file-one | + b66a0bf origin/main add initial-file | + | + | + | + | + | + | + | + | + | + | + | + | +────────────────────────────────────────────────────────────────────────────────| +$ git apply --reverse | +styles_hash: 127ac9ef0aee93e + +[file before] +blahonga +BAR +BAZ + +[file after] +FOO +BAR +BAZ diff --git a/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_hunk.snap b/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_hunk.snap new file mode 100644 index 0000000000..c8dece537d --- /dev/null +++ b/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_hunk.snap @@ -0,0 +1,35 @@ +--- +source: src/tests/reverse.rs +expression: out +--- + On branch main | + Your branch is ahead of 'origin/main' by 1 commit(s). | + | + Recent commits | + 9f4a45e main add file-one | +▌b66a0bf origin/main add initial-file | + | + | + | + | + | + | + | + | + | + | + | + | +────────────────────────────────────────────────────────────────────────────────| +$ git apply --reverse | +styles_hash: 80fde90219deddc2 + +[file before] +blahonga +BAR +BAZ + +[file after] +FOO +BAR +BAZ diff --git a/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_line.snap b/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_line.snap new file mode 100644 index 0000000000..a074a58e27 --- /dev/null +++ b/src/tests/snapshots/gitu__tests__reverse__reverse_unstaged_line.snap @@ -0,0 +1,36 @@ +--- +source: src/tests/reverse.rs +expression: out +--- + On branch main | + Your branch is ahead of 'origin/main' by 1 commit(s). | + | + Unstaged changes (1) | + modified file-one | +▌@@ -1,3 +1,4 @@ | +▌ FOO | +▌+blahonga | +▌ BAR | +▌ BAZ | + | + Recent commits | + 9f4a45e main add file-one | + b66a0bf origin/main add initial-file | + | + | + | + | +────────────────────────────────────────────────────────────────────────────────| +$ git apply --reverse --recount | +styles_hash: 8f5ab6e7fae3f786 + +[file before] +blahonga +BAR +BAZ + +[file after] +FOO +blahonga +BAR +BAZ