Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions crates/but/src/rub/commits.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<DiffSpec>>();

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::<Vec<DiffSpec>>();

// 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::<Vec<but_core::TreeChange>>,
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::<Vec<but_core::TreeChange>>,
None,
)?;

let before_assignments = before_assignments
.into_iter()
.filter_map(|a| a.id)
.collect::<HashSet<_>>();

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::<Vec<_>>();

but_hunk_assignment::assign(ctx, to_assign, None)?;

println!("Uncommitted changes");

Ok(())
}
63 changes: 42 additions & 21 deletions crates/but/src/rub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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<String> = source_result.iter().map(|id| {
match id {
CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]),
let matches: Vec<String> = 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,
Expand All @@ -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<String> = target_result.iter().map(|id| {
match id {
CliId::Commit { oid } => format!("{} (commit {})", id.to_string(), &oid.to_string()[..7]),
let matches: Vec<String> = 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,
Expand Down