diff --git a/crates/but/src/rub/commits.rs b/crates/but/src/rub/commits.rs new file mode 100644 index 0000000000..89e0e49a1e --- /dev/null +++ b/crates/but/src/rub/commits.rs @@ -0,0 +1,127 @@ +use std::collections::HashSet; + +use anyhow::{Context, Result}; +use bstr::ByteSlice; +use but_core::diff::tree_changes; +use but_hunk_assignment::HunkAssignmentRequest; +use but_workspace::DiffSpec; +use gitbutler_branch_actions::update_workspace_commit; +use gitbutler_command_context::CommandContext; +use gitbutler_stack::VirtualBranchesHandle; + +use crate::rub::{assign::branch_name_to_stack_id, undo::stack_id_by_commit_id}; + +pub fn commited_file_to_another_commit( + ctx: &mut CommandContext, + path: &str, + source_id: gix::ObjectId, + target_id: gix::ObjectId, +) -> Result<()> { + let source_stack = stack_id_by_commit_id(ctx, &source_id)?; + let target_stack = stack_id_by_commit_id(ctx, &target_id)?; + + let repo = ctx.gix_repo()?; + let source_commit = repo.find_commit(source_id)?; + let source_commit_parent_id = source_commit.parent_ids().next().context("First parent")?; + + let (tree_changes, _) = tree_changes(&repo, Some(source_commit_parent_id.detach()), source_id)?; + let relevant_changes = tree_changes + .into_iter() + .filter(|tc| tc.path.to_str_lossy() == path) + .map(Into::into) + .collect::>(); + + but_workspace::move_changes_between_commits( + ctx, + source_stack, + source_id, + target_stack, + target_id, + relevant_changes, + ctx.app_settings().context_lines, + )?; + + let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir()); + update_workspace_commit(&vb_state, &ctx)?; + + println!("Moved files between commits!"); + + Ok(()) +} + +pub fn commited_file_to_unassigned_stack( + ctx: &mut CommandContext, + path: &str, + source_id: gix::ObjectId, + target_branch: &str, +) -> Result<()> { + let source_stack = stack_id_by_commit_id(ctx, &source_id)?; + let target_stack = branch_name_to_stack_id(ctx, Some(target_branch))?; + + let repo = ctx.gix_repo()?; + + let source_commit = repo.find_commit(source_id)?; + let source_commit_parent_id = source_commit.parent_ids().next().context("First parent")?; + + let (tree_changes, _) = tree_changes(&repo, Some(source_commit_parent_id.detach()), source_id)?; + let relevant_changes = tree_changes + .into_iter() + .filter(|tc| tc.path.to_str_lossy() == path) + .map(Into::into) + .collect::>(); + + // If we want to assign the changes after uncommitting, we could try to do + // something with the hunk headers, but this is not precise as the hunk + // headers might have changed from what they were like when they were + // committed. + // + // As such, we take all the old assignments, and all the new assignments from after the + // uncommit, and find the ones that are not present in the old assignments. + // We then convert those into assignment requests for the given stack. + let before_assignments = but_hunk_assignment::assignments_with_fallback( + ctx, + false, + None::>, + None, + )? + .0; + + but_workspace::remove_changes_from_commit_in_stack( + &ctx, + source_stack, + source_id, + relevant_changes, + ctx.app_settings().context_lines, + )?; + + let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir()); + update_workspace_commit(&vb_state, ctx)?; + + let (after_assignments, _) = but_hunk_assignment::assignments_with_fallback( + ctx, + false, + None::>, + None, + )?; + + let before_assignments = before_assignments + .into_iter() + .filter_map(|a| a.id) + .collect::>(); + + let to_assign = after_assignments + .into_iter() + .filter(|a| a.id.is_some_and(|id| !before_assignments.contains(&id))) + .map(|a| HunkAssignmentRequest { + hunk_header: a.hunk_header, + path_bytes: a.path_bytes, + stack_id: target_stack, + }) + .collect::>(); + + but_hunk_assignment::assign(ctx, to_assign, None)?; + + println!("Uncommitted changes"); + + Ok(()) +} diff --git a/crates/but/src/rub/mod.rs b/crates/but/src/rub/mod.rs index 99f6de7449..bdb463c1bd 100644 --- a/crates/but/src/rub/mod.rs +++ b/crates/but/src/rub/mod.rs @@ -7,10 +7,11 @@ use gitbutler_command_context::CommandContext; use gitbutler_project::Project; mod amend; mod assign; +mod commits; mod move_commit; mod squash; -mod undo; mod uncommit; +mod undo; use crate::id::CliId; @@ -50,13 +51,27 @@ pub(crate) fn handle( (CliId::CommittedFile { path, commit_oid }, CliId::Unassigned) => { uncommit::file_from_commit(ctx, path, commit_oid) } - (CliId::CommittedFile { .. }, CliId::Branch { .. }) => { - // Extract file from commit to branch - for now, not implemented - bail!("Extracting files from commits is not yet supported. Use git commands to extract file changes.") - } - (CliId::CommittedFile { .. }, CliId::Commit { .. }) => { + ( + CliId::CommittedFile { + path, + commit_oid: source_id, + }, + CliId::Branch { + name: target_branch, + }, + ) => { + // Extract file from commit to branch - for now, not implemented + commits::commited_file_to_unassigned_stack(ctx, path, *source_id, target_branch) + } + ( + CliId::CommittedFile { + path, + commit_oid: source_id, + }, + CliId::Commit { oid: target_id }, + ) => { // Move file from one commit to another - for now, not implemented - bail!("Moving files between commits is not yet supported. Use git commands to modify commits.") + commits::commited_file_to_another_commit(ctx, path, *source_id, *target_id) } (CliId::Unassigned, CliId::UncommittedFile { .. }) => { bail!(makes_no_sense_error(&source, &target)) @@ -113,17 +128,20 @@ fn ids(ctx: &mut CommandContext, source: &str, target: &str) -> anyhow::Result<( if source_result.len() != 1 { if source_result.is_empty() { return Err(anyhow::anyhow!( - "Source '{}' not found. If you just performed a Git operation (squash, rebase, etc.), try running 'but status' to refresh the current state.", + "Source '{}' not found. If you just performed a Git operation (squash, rebase, etc.), try running 'but status' to refresh the current state.", source )); } else { - let matches: Vec = source_result.iter().map(|id| { - match id { - CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]), + let matches: Vec = source_result + .iter() + .map(|id| match id { + CliId::Commit { oid } => { + format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]) + } CliId::Branch { name } => format!("{} (branch '{}')", id.to_string(), name), - _ => format!("{} ({})", id.to_string(), id.kind()) - } - }).collect(); + _ => format!("{} ({})", id.to_string(), id.kind()), + }) + .collect(); return Err(anyhow::anyhow!( "Source '{}' is ambiguous. Matches: {}. Try using more characters, a longer SHA, or the full branch name to disambiguate.", source, @@ -135,17 +153,20 @@ fn ids(ctx: &mut CommandContext, source: &str, target: &str) -> anyhow::Result<( if target_result.len() != 1 { if target_result.is_empty() { return Err(anyhow::anyhow!( - "Target '{}' not found. If you just performed a Git operation (squash, rebase, etc.), try running 'but status' to refresh the current state.", + "Target '{}' not found. If you just performed a Git operation (squash, rebase, etc.), try running 'but status' to refresh the current state.", target )); } else { - let matches: Vec = target_result.iter().map(|id| { - match id { - CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]), + let matches: Vec = target_result + .iter() + .map(|id| match id { + CliId::Commit { oid } => { + format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]) + } CliId::Branch { name } => format!("{} (branch '{}')", id.to_string(), name), - _ => format!("{} ({})", id.to_string(), id.kind()) - } - }).collect(); + _ => format!("{} ({})", id.to_string(), id.kind()), + }) + .collect(); return Err(anyhow::anyhow!( "Target '{}' is ambiguous. Matches: {}. Try using more characters, a longer SHA, or the full branch name to disambiguate.", target,