diff --git a/crates/but-cli/src/args.rs b/crates/but-cli/src/args.rs index 98898eaeb8..465727e5fb 100644 --- a/crates/but-cli/src/args.rs +++ b/crates/but-cli/src/args.rs @@ -46,14 +46,18 @@ pub enum Subcommands { /// List all uncommitted working tree changes. Status { /// Also compute unified diffs for each tree-change. - #[clap(long, short = 'c', default_value_t = 3)] + #[clap(long, short = 'c', default_value_t = crate::command::UI_CONTEXT_LINES)] context_lines: u32, /// Also compute unified diffs for each tree-change. #[clap(long, short = 'd')] unified_diff: bool, }, /// Discard the specified worktree change. + #[clap(disable_help_flag(true))] DiscardChange { + /// The zero-based indices of all hunks to discard. + #[clap(long, short = 'h')] + hunk_indices: Vec, /// The repo-relative path to the changed file to discard. current_path: PathBuf, /// If the change is a rename, identify the repo-relative path of the source. diff --git a/crates/but-cli/src/command/mod.rs b/crates/but-cli/src/command/mod.rs index 26adf7aa0d..f5559a6bed 100644 --- a/crates/but-cli/src/command/mod.rs +++ b/crates/but-cli/src/command/mod.rs @@ -1,9 +1,11 @@ -use anyhow::{Context, bail}; +use anyhow::{Context, anyhow, bail}; +use but_core::UnifiedDiff; +use but_workspace::commit_engine::HunkHeader; use gitbutler_project::Project; -use gix::bstr::BString; +use gix::bstr::{BString, ByteSlice}; use std::path::Path; -const UI_CONTEXT_LINES: u32 = 3; +pub(crate) const UI_CONTEXT_LINES: u32 = 3; pub fn project_from_path(path: &Path) -> anyhow::Result { Project::from_path(path) @@ -140,13 +142,44 @@ pub(crate) fn discard_change( cwd: &Path, current_rela_path: &Path, previous_rela_path: Option<&Path>, + hunk_indices: &[usize], ) -> anyhow::Result<()> { let repo = gix::discover(cwd)?; + let previous_path = previous_rela_path.map(path_to_rela_path).transpose()?; + let path = path_to_rela_path(current_rela_path)?; + let hunk_headers = if hunk_indices.is_empty() { + vec![] + } else { + let worktree_changes = but_core::diff::worktree_changes(&repo)? + .changes + .into_iter() + .find(|change| { + change.path == path + && change.previous_path() == previous_path.as_ref().map(|p| p.as_bstr()) + }).with_context(|| format!("Couldn't find worktree change for file at '{path}' (previous-path: {previous_path:?}"))?; + let UnifiedDiff::Patch { hunks } = + worktree_changes.unified_diff(&repo, UI_CONTEXT_LINES)? + else { + bail!("No hunks available for given '{path}'") + }; + + hunk_indices + .iter() + .map(|idx| { + hunks.get(*idx).cloned().map(Into::into).ok_or_else(|| { + anyhow!( + "There was no hunk at index {idx} in '{path}' with {} hunks", + hunks.len() + ) + }) + }) + .collect::, _>>()? + }; let spec = but_workspace::commit_engine::DiffSpec { - previous_path: previous_rela_path.map(path_to_rela_path).transpose()?, - path: path_to_rela_path(current_rela_path)?, - hunk_headers: vec![], + previous_path, + path, + hunk_headers, }; debug_print(but_workspace::discard_workspace_changes( &repo, diff --git a/crates/but-cli/src/main.rs b/crates/but-cli/src/main.rs index 2d4ffa5d5f..32fab69acf 100644 --- a/crates/but-cli/src/main.rs +++ b/crates/but-cli/src/main.rs @@ -17,9 +17,15 @@ fn main() -> Result<()> { match &args.cmd { args::Subcommands::DiscardChange { + hunk_indices, current_path, previous_path, - } => command::discard_change(&args.current_dir, current_path, previous_path.as_deref()), + } => command::discard_change( + &args.current_dir, + current_path, + previous_path.as_deref(), + hunk_indices, + ), args::Subcommands::Commit { message, amend, diff --git a/crates/but-workspace/src/commit_engine/hunks.rs b/crates/but-workspace/src/commit_engine/hunks.rs index 5b92cbe780..1033fa5e66 100644 --- a/crates/but-workspace/src/commit_engine/hunks.rs +++ b/crates/but-workspace/src/commit_engine/hunks.rs @@ -14,9 +14,9 @@ pub fn apply_hunks( new_image: &BStr, hunks: &[HunkHeader], ) -> anyhow::Result { - let mut worktree_base_cursor = 1; /* 1-based counting */ + let mut old_cursor = 1; /* 1-based counting */ let mut old_iter = old_image.lines_with_terminator(); - let mut worktree_actual_cursor = 1; /* 1-based counting */ + let mut new_cursor = 1; /* 1-based counting */ let mut new_iter = new_image.lines_with_terminator(); let mut result_image: BString = Vec::with_capacity(old_image.len().max(new_image.len())).into(); @@ -26,11 +26,10 @@ pub fn apply_hunks( // Write the new hunk. // Repeat for each hunk, and write all remaining old lines. for selected_hunk in hunks { - let catchup_base_lines = old_iter.by_ref().take( - (selected_hunk.old_start as usize) - .checked_sub(worktree_base_cursor) - .context("hunks must be in order from top to bottom of the file")?, - ); + let old_skips = (selected_hunk.old_start as usize) + .checked_sub(old_cursor) + .context("hunks must be in order from top to bottom of the file")?; + let catchup_base_lines = old_iter.by_ref().take(old_skips); for line in catchup_base_lines { result_image.extend_from_slice(line); } @@ -38,21 +37,19 @@ pub fn apply_hunks( .by_ref() .take(selected_hunk.old_lines as usize) .count(); - worktree_base_cursor += selected_hunk.old_lines as usize; + old_cursor += old_skips + selected_hunk.old_lines as usize; + let new_skips = (selected_hunk.new_start as usize) + .checked_sub(new_cursor) + .context("hunks for new lines must be in order")?; let new_hunk_lines = new_iter .by_ref() - .skip( - (selected_hunk.new_start as usize) - .checked_sub(worktree_actual_cursor) - .context("hunks for new lines must be in order")?, - ) + .skip(new_skips) .take(selected_hunk.new_lines as usize); - for line in new_hunk_lines { - result_image.extend_from_slice(line); + result_image.extend_from_slice(line.as_bstr()); } - worktree_actual_cursor += selected_hunk.new_lines as usize; + new_cursor += new_skips + selected_hunk.new_lines as usize; } for line in old_iter { diff --git a/crates/but-workspace/tests/fixtures/scenario/mixed-hunk-modifications.sh b/crates/but-workspace/tests/fixtures/scenario/mixed-hunk-modifications.sh index 3349aa70ea..948a477195 100644 --- a/crates/but-workspace/tests/fixtures/scenario/mixed-hunk-modifications.sh +++ b/crates/but-workspace/tests/fixtures/scenario/mixed-hunk-modifications.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash ### Description -# A single tracked file, modified in the workspace to have: -# - added lines at the beginning -# - deleted lines at the end -# - modified lines, added lines, and deleted lines directly after one another in the middle. +# Various files, of which two are in the worktree (with changes) and in the index (with changes). +# Another pair of files was deleted, both in the worktree and in the index. +# Lastly, each of these have specific worktree modifications in multiple hunks, and a plain file was made +# executable in the index, and another rename destination was made executable, too. +# Lastly, a simple file was executable, which isn't executable anymore. set -eu -o pipefail git init -seq 5 18 >file +seq 5 18 >file && chmod +x file seq 5 18 >file-in-index seq 5 18 >file-to-be-renamed seq 5 18 >file-to-be-renamed-in-index @@ -33,10 +34,17 @@ eleven 16 EOF -cp file file-in-index && git add file-in-index +chmod -x file +cp file file-in-index && \ + chmod +x file-in-index && \ + git add file-in-index -seq 2 18 >file-to-be-renamed && mv file-to-be-renamed file-renamed -cp file file-to-be-renamed-in-index && git mv file-to-be-renamed-in-index file-renamed-in-index + +seq 2 18 >file-to-be-renamed && \ + mv file-to-be-renamed file-renamed && \ + chmod +x file-renamed +cp file file-to-be-renamed-in-index && \ + git mv file-to-be-renamed-in-index file-renamed-in-index diff --git a/crates/but-workspace/tests/workspace/discard/file.rs b/crates/but-workspace/tests/workspace/discard/file.rs index e8a1aae359..ce494b4734 100644 --- a/crates/but-workspace/tests/workspace/discard/file.rs +++ b/crates/but-workspace/tests/workspace/discard/file.rs @@ -969,7 +969,7 @@ R a/b/link -> a/sibling/link Ok(()) } -// Copy of `all_file_types_deleted_in_worktree`, but can't be loop due to `insta`. +// Copy of `all_file_types_deleted_in_worktree`, could also be a loop but insta::allow_duplicates!() isn't pretty. #[test] #[cfg(unix)] fn all_file_types_deleted_in_index() -> anyhow::Result<()> { diff --git a/crates/but-workspace/tests/workspace/discard/hunk.rs b/crates/but-workspace/tests/workspace/discard/hunk.rs index 646c81c1ce..095842136e 100644 --- a/crates/but-workspace/tests/workspace/discard/hunk.rs +++ b/crates/but-workspace/tests/workspace/discard/hunk.rs @@ -1,10 +1,12 @@ -use crate::discard::hunk::util::hunk_header; +use crate::discard::hunk::util::{hunk_header, previous_change_text}; use crate::utils::{ CONTEXT_LINES, read_only_in_memory_scenario, to_change_specs_all_hunks, visualize_index, writable_scenario, }; -use but_testsupport::git_status; -use but_workspace::commit_engine::DiffSpec; +use bstr::{BString, ByteSlice}; +use but_core::UnifiedDiff; +use but_testsupport::{git_status, visualize_disk_tree_skip_dot_git}; +use but_workspace::commit_engine::{DiffSpec, HunkHeader}; use but_workspace::discard_workspace_changes; #[test] @@ -48,6 +50,235 @@ fn non_modifications_trigger_error() -> anyhow::Result<()> { } #[test] +fn hunk_removal_from_end() -> anyhow::Result<()> { + let (repo, _tmp) = writable_scenario("mixed-hunk-modifications"); + let mut hunk_info = Vec::new(); + let filename = "file-in-index"; + let file_content = || std::fs::read(repo.workdir().unwrap().join(filename)).map(BString::from); + insta::assert_snapshot!(file_content()?, @r" + 1 + 2 + 3 + 4 + 5 + 6-7 + 8 + 9 + ten + eleven + 12 + 20 + 21 + 22 + 15 + 16 + "); + while let Some(change) = but_core::diff::worktree_changes(&repo)? + .changes + .into_iter() + .find(|change| change.path == "file-in-index") + { + let previous_text = previous_change_text(&repo, &change)?; + insta::allow_duplicates!(insta::assert_snapshot!(previous_text, @r" + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + ")); + let UnifiedDiff::Patch { mut hunks } = change.unified_diff(&repo, CONTEXT_LINES)? else { + unreachable!("We know there are hunks") + }; + assert_ne!( + hunks.len(), + 0, + "the reason we see it is file modifications: {change:#?}" + ); + + let before = file_content()?; + let mut last_hunk = hunks + .pop() + .expect("there is always one change if the file is only modified"); + let discarded_patch = std::mem::take(&mut last_hunk.diff); + let discard_spec = DiffSpec { + previous_path: None, + path: change.path.clone(), + hunk_headers: vec![last_hunk.into()], + }; + let dropped = discard_workspace_changes(&repo, Some(discard_spec.into()), CONTEXT_LINES)?; + assert_eq!( + dropped.len(), + 0, + "the hunk could be found and was discarded" + ); + let after = file_content()?; + hunk_info.push((before, discarded_patch, after)); + } + + insta::assert_debug_snapshot!(hunk_info, @r#" + [ + ( + "1\n2\n3\n4\n5\n6-7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n", + "@@ -13,2 +17,0 @@\n-17\n-18\n", + "1\n2\n3\n4\n5\n6-7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n17\n18\n", + ), + ( + "1\n2\n3\n4\n5\n6-7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n17\n18\n", + "@@ -9,2 +12,3 @@\n-13\n-14\n+20\n+21\n+22\n", + "1\n2\n3\n4\n5\n6-7\n8\n9\nten\neleven\n12\n13\n14\n15\n16\n17\n18\n", + ), + ( + "1\n2\n3\n4\n5\n6-7\n8\n9\nten\neleven\n12\n13\n14\n15\n16\n17\n18\n", + "@@ -6,2 +9,2 @@\n-10\n-11\n+ten\n+eleven\n", + "1\n2\n3\n4\n5\n6-7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n", + ), + ( + "1\n2\n3\n4\n5\n6-7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n", + "@@ -2,2 +6,1 @@\n-6\n-7\n+6-7\n", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n", + ), + ( + "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n", + "@@ -1,0 +1,4 @@\n+1\n+2\n+3\n+4\n", + "5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n", + ), + ] + "#); + Ok(()) +} + +#[test] +fn dropped_hunks() -> anyhow::Result<()> { + let (repo, _tmp) = writable_scenario("mixed-hunk-modifications"); + let change = but_core::diff::worktree_changes(&repo)? + .changes + .into_iter() + .find(|change| change.path == "file") + .expect("known to have modifications"); + + let UnifiedDiff::Patch { hunks } = change.unified_diff(&repo, CONTEXT_LINES)? else { + unreachable!("We know there are hunks") + }; + + let mut hunks_to_discard: Vec = hunks.into_iter().map(Into::into).collect(); + hunks_to_discard.push(hunk_header("-10,1", "+13,3")); + hunks_to_discard.insert(0, hunk_header("-1,1", "+1,0")); + + let discard_spec = DiffSpec { + previous_path: None, + path: change.path, + hunk_headers: hunks_to_discard, + }; + let dropped = discard_workspace_changes(&repo, Some(discard_spec.into()), CONTEXT_LINES)?; + // It drops just the two missing ones hunks + insta::assert_debug_snapshot!(dropped, @r#" + [ + DiscardSpec( + DiffSpec { + previous_path: None, + path: "file", + hunk_headers: [ + HunkHeader { + old_start: 1, + old_lines: 1, + new_start: 1, + new_lines: 0, + }, + HunkHeader { + old_start: 10, + old_lines: 1, + new_start: 13, + new_lines: 3, + }, + ], + }, + ), + ] + "#); + Ok(()) +} + +#[test] +fn hunk_removal_from_beginning() -> anyhow::Result<()> { + let (repo, _tmp) = writable_scenario("mixed-hunk-modifications"); + let mut hunk_info = Vec::new(); + let filename = "file-in-index"; + let file_content = || std::fs::read(repo.workdir().unwrap().join(filename)).map(BString::from); + while let Some(change) = but_core::diff::worktree_changes(&repo)? + .changes + .into_iter() + .find(|change| change.path == "file-in-index") + { + let UnifiedDiff::Patch { mut hunks } = change.unified_diff(&repo, CONTEXT_LINES)? else { + unreachable!("We know there are hunks") + }; + assert_ne!( + hunks.len(), + 0, + "the reason we see it is file modifications: {change:#?}" + ); + + let before = file_content()?; + let mut first_hun_hunk = hunks.remove(0); + let discarded_patch = std::mem::take(&mut first_hun_hunk.diff); + let discard_spec = DiffSpec { + previous_path: None, + path: change.path.clone(), + hunk_headers: vec![first_hun_hunk.into()], + }; + let dropped = discard_workspace_changes(&repo, Some(discard_spec.into()), CONTEXT_LINES)?; + assert_eq!( + dropped.len(), + 0, + "the hunk could be found and was discarded" + ); + let after = file_content()?; + hunk_info.push((before, discarded_patch, after)); + } + + insta::assert_debug_snapshot!(hunk_info, @r#" + [ + ( + "1\n2\n3\n4\n5\n6-7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n", + "@@ -1,0 +1,4 @@\n+1\n+2\n+3\n+4\n", + "5\n6-7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n", + ), + ( + "5\n6-7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n", + "@@ -2,2 +2,1 @@\n-6\n-7\n+6-7\n", + "5\n6\n7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n", + ), + ( + "5\n6\n7\n8\n9\nten\neleven\n12\n20\n21\n22\n15\n16\n", + "@@ -6,2 +6,2 @@\n-10\n-11\n+ten\n+eleven\n", + "5\n6\n7\n8\n9\n10\n11\n12\n20\n21\n22\n15\n16\n", + ), + ( + "5\n6\n7\n8\n9\n10\n11\n12\n20\n21\n22\n15\n16\n", + "@@ -9,2 +9,3 @@\n-13\n-14\n+20\n+21\n+22\n", + "5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n", + ), + ( + "5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n", + "@@ -13,2 +13,0 @@\n-17\n-18\n", + "5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n", + ), + ] + "#); + Ok(()) +} + +#[test] +#[cfg(unix)] fn deletion_modification_addition_of_hunks_mixed_discard_all_in_workspace() -> anyhow::Result<()> { let (repo, _tmp) = writable_scenario("mixed-hunk-modifications"); // Note that one of these renames can't be detected by Git but is visible to us. @@ -59,69 +290,100 @@ fn deletion_modification_addition_of_hunks_mixed_discard_all_in_workspace() -> a ?? file-renamed "); + insta::assert_snapshot!(visualize_index(&**repo.index()?), @r" + 100755:3d3b36f file + 100755:cb89473 file-in-index + 100644:3d3b36f file-renamed-in-index + 100644:3d3b36f file-to-be-renamed + "); + + let workdir = repo.workdir().unwrap(); + insta::assert_snapshot!(visualize_disk_tree_skip_dot_git(workdir)?, @r" + . + ├── .git:40755 + ├── file:100644 + ├── file-in-index:100755 + ├── file-renamed:100755 + └── file-renamed-in-index:100644 + "); + // Show that we detect renames correctly, despite the rename + modification. let wt_changes = but_core::diff::worktree_changes(&repo)?; - insta::assert_debug_snapshot!(wt_changes.changes, @r#" - [ - TreeChange { - path: "file", - status: Modification { - previous_state: ChangeState { - id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), - kind: Blob, - }, - state: ChangeState { - id: Sha1(0000000000000000000000000000000000000000), - kind: Blob, + insta::assert_debug_snapshot!(wt_changes, @r#" + WorktreeChanges { + changes: [ + TreeChange { + path: "file", + status: Modification { + previous_state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: BlobExecutable, + }, + state: ChangeState { + id: Sha1(0000000000000000000000000000000000000000), + kind: Blob, + }, + flags: Some( + ExecutableBitRemoved, + ), }, - flags: None, }, - }, - TreeChange { - path: "file-in-index", - status: Modification { - previous_state: ChangeState { - id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), - kind: Blob, - }, - state: ChangeState { - id: Sha1(cb89473a55c3443b5567e990e2a0293895c91a4a), - kind: Blob, + TreeChange { + path: "file-in-index", + status: Modification { + previous_state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: Blob, + }, + state: ChangeState { + id: Sha1(cb89473a55c3443b5567e990e2a0293895c91a4a), + kind: BlobExecutable, + }, + flags: Some( + ExecutableBitAdded, + ), }, - flags: None, }, - }, - TreeChange { - path: "file-renamed", - status: Rename { - previous_path: "file-to-be-renamed", - previous_state: ChangeState { - id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), - kind: Blob, + TreeChange { + path: "file-renamed", + status: Rename { + previous_path: "file-to-be-renamed", + previous_state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: Blob, + }, + state: ChangeState { + id: Sha1(0000000000000000000000000000000000000000), + kind: BlobExecutable, + }, + flags: Some( + ExecutableBitAdded, + ), }, - state: ChangeState { - id: Sha1(0000000000000000000000000000000000000000), - kind: Blob, - }, - flags: None, }, - }, - TreeChange { - path: "file-renamed-in-index", - status: Rename { - previous_path: "file-to-be-renamed-in-index", - previous_state: ChangeState { - id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), - kind: Blob, - }, - state: ChangeState { - id: Sha1(0000000000000000000000000000000000000000), - kind: Blob, + TreeChange { + path: "file-renamed-in-index", + status: Rename { + previous_path: "file-to-be-renamed-in-index", + previous_state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: Blob, + }, + state: ChangeState { + id: Sha1(0000000000000000000000000000000000000000), + kind: Blob, + }, + flags: None, }, - flags: None, }, - }, - ] + ], + ignored_changes: [ + IgnoredWorktreeChange { + path: "file-renamed-in-index", + status: TreeIndex, + }, + ], + } "#); let specs = to_change_specs_all_hunks(&repo, wt_changes)?; @@ -129,9 +391,32 @@ fn deletion_modification_addition_of_hunks_mixed_discard_all_in_workspace() -> a discard_workspace_changes(&repo, specs.into_iter().map(Into::into), CONTEXT_LINES)?; assert!(dropped.is_empty()); - // TODO: this most definitely isn't correct, figure out what's going on. + insta::assert_snapshot!(visualize_disk_tree_skip_dot_git(repo.workdir().unwrap())?, @r" + . + ├── .git:40755 + ├── file:100644 + ├── file-in-index:100755 + ├── file-renamed:100755 + └── file-renamed-in-index:100644 + "); + + for filename in [ + "file", + "file-in-index", + "file-renamed", + "file-renamed-in-index", + ] { + let content = std::fs::read(workdir.join(filename))?; + assert_eq!( + content.as_bstr(), + "5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n", + "{filename}: All files have the same content after worktree-discards" + ); + } + // Notably, discarding all hunks leaves the renamed file in place, but without modifications. insta::assert_snapshot!(git_status(&repo)?, @r" + M file MM file-in-index R file-to-be-renamed-in-index -> file-renamed-in-index D file-to-be-renamed @@ -139,19 +424,100 @@ fn deletion_modification_addition_of_hunks_mixed_discard_all_in_workspace() -> a "); // The index still only holds what was in the index before, but is representing the changed worktree. insta::assert_snapshot!(visualize_index(&**repo.index()?), @r" - 100644:3d3b36f file - 100644:cb89473 file-in-index + 100755:3d3b36f file + 100755:cb89473 file-in-index 100644:3d3b36f file-renamed-in-index 100644:3d3b36f file-to-be-renamed "); - // TODO: content checks. + // The index is transparent, so `file-in-index` was reverted to the version in the `HEAD^{tree}` + let wt_changes = but_core::diff::worktree_changes(&repo)?; + insta::assert_debug_snapshot!(wt_changes, @r#" + WorktreeChanges { + changes: [ + TreeChange { + path: "file", + status: Modification { + previous_state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: BlobExecutable, + }, + state: ChangeState { + id: Sha1(0000000000000000000000000000000000000000), + kind: Blob, + }, + flags: Some( + ExecutableBitRemoved, + ), + }, + }, + TreeChange { + path: "file-renamed", + status: Rename { + previous_path: "file-to-be-renamed", + previous_state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: Blob, + }, + state: ChangeState { + id: Sha1(0000000000000000000000000000000000000000), + kind: BlobExecutable, + }, + flags: Some( + ExecutableBitAdded, + ), + }, + }, + TreeChange { + path: "file-renamed-in-index", + status: Rename { + previous_path: "file-to-be-renamed-in-index", + previous_state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: Blob, + }, + state: ChangeState { + id: Sha1(3d3b36f021391fa57312d7dfd1ad8cf5a13dca6d), + kind: Blob, + }, + flags: None, + }, + }, + ], + ignored_changes: [ + IgnoredWorktreeChange { + path: "file-in-index", + status: TreeIndexWorktreeChangeIneffective, + }, + ], + } + "#); Ok(()) } mod util { + use bstr::BString; + use but_core::TreeChange; use but_workspace::commit_engine::HunkHeader; + use gix::prelude::ObjectIdExt; + + pub fn previous_change_text( + repo: &gix::Repository, + change: &TreeChange, + ) -> anyhow::Result { + Ok(change + .status + .previous_state_and_path() + .expect("modification") + .0 + .id + .attach(repo) + .object()? + .detach() + .data + .into()) + } /// Choose a slightly more obvious, yet easy to type syntax than a function with 4 parameters. pub fn hunk_header(old: &str, new: &str) -> HunkHeader {